blob: e246975bba919714c8d4e115609fd3b71522f60b [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.ide.common.resources.platform;
import static com.android.SdkConstants.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.PREFIX_THEME_REF;
import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.ide.common.api.IAttributeInfo.Format.BOOLEAN;
import static com.android.ide.common.api.IAttributeInfo.Format.COLOR;
import static com.android.ide.common.api.IAttributeInfo.Format.DIMENSION;
import static com.android.ide.common.api.IAttributeInfo.Format.ENUM;
import static com.android.ide.common.api.IAttributeInfo.Format.FLAG;
import static com.android.ide.common.api.IAttributeInfo.Format.FLOAT;
import static com.android.ide.common.api.IAttributeInfo.Format.FRACTION;
import static com.android.ide.common.api.IAttributeInfo.Format.INTEGER;
import static com.android.ide.common.api.IAttributeInfo.Format.STRING;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.api.IAttributeInfo;
import com.android.ide.common.resources.ResourceRepository;
import com.android.resources.ResourceType;
import com.google.common.base.Splitter;
import java.util.EnumSet;
import java.util.regex.Pattern;
/**
* Information about an attribute as gathered from the attrs.xml file where
* the attribute was declared. This must include a format (string, reference, float, etc.),
* possible flag or enum values, whether it's deprecated and its javadoc.
*/
public class AttributeInfo implements IAttributeInfo {
/** XML Name of the attribute */
private String mName;
/** Formats of the attribute. Cannot be null. Should have at least one format. */
private EnumSet<Format> mFormats;
/** Values for enum. null for other types. */
private String[] mEnumValues;
/** Values for flag. null for other types. */
private String[] mFlagValues;
/** Short javadoc (i.e. the first sentence). */
private String mJavaDoc;
/** Documentation for deprecated attributes. Null if not deprecated. */
private String mDeprecatedDoc;
/** The source class defining this attribute */
private String mDefinedBy;
/**
* @param name The XML Name of the attribute
* @param formats The formats of the attribute. Cannot be null.
* Should have at least one format.
*/
public AttributeInfo(@NonNull String name, @NonNull EnumSet<Format> formats) {
mName = name;
mFormats = formats;
}
/**
* @param name The XML Name of the attribute
* @param formats The formats of the attribute. Cannot be null.
* Should have at least one format.
* @param javadoc Short javadoc (i.e. the first sentence).
*/
public AttributeInfo(@NonNull String name, @NonNull EnumSet<Format> formats, String javadoc) {
mName = name;
mFormats = formats;
mJavaDoc = javadoc;
}
public AttributeInfo(AttributeInfo info) {
mName = info.mName;
mFormats = info.mFormats;
mEnumValues = info.mEnumValues;
mFlagValues = info.mFlagValues;
mJavaDoc = info.mJavaDoc;
mDeprecatedDoc = info.mDeprecatedDoc;
}
/**
* Sets the XML Name of the attribute
*
* @param name the new name to assign
*/
public void setName(String name) {
mName = name;
}
/** Returns the XML Name of the attribute */
@Override
public @NonNull String getName() {
return mName;
}
/** Returns the formats of the attribute. Cannot be null.
* Should have at least one format. */
@Override
public @NonNull EnumSet<Format> getFormats() {
return mFormats;
}
/** Returns the values for enums. null for other types. */
@Override
public String[] getEnumValues() {
return mEnumValues;
}
/** Returns the values for flags. null for other types. */
@Override
public String[] getFlagValues() {
return mFlagValues;
}
/** Returns a short javadoc, .i.e. the first sentence. */
@Override
public @NonNull String getJavaDoc() {
return mJavaDoc;
}
/** Returns the documentation for deprecated attributes. Null if not deprecated. */
@Override
public String getDeprecatedDoc() {
return mDeprecatedDoc;
}
/** Sets the values for enums. null for other types. */
public AttributeInfo setEnumValues(String[] values) {
mEnumValues = values;
return this;
}
/** Sets the values for flags. null for other types. */
public AttributeInfo setFlagValues(String[] values) {
mFlagValues = values;
return this;
}
/** Sets a short javadoc, .i.e. the first sentence. */
public void setJavaDoc(String javaDoc) {
mJavaDoc = javaDoc;
}
/** Sets the documentation for deprecated attributes. Null if not deprecated. */
public void setDeprecatedDoc(String deprecatedDoc) {
mDeprecatedDoc = deprecatedDoc;
}
/**
* Sets the name of the class (fully qualified class name) which defined
* this attribute
*
* @param definedBy the name of the class (fully qualified class name) which
* defined this attribute
*/
public void setDefinedBy(String definedBy) {
mDefinedBy = definedBy;
}
/**
* Returns the name of the class (fully qualified class name) which defined
* this attribute
*
* @return the name of the class (fully qualified class name) which defined
* this attribute
*/
@Override
public @NonNull String getDefinedBy() {
return mDefinedBy;
}
private final static Pattern INTEGER_PATTERN = Pattern.compile("-?[0-9]+"); //$NON-NLS-1$
private final static Pattern FLOAT_PATTERN =
Pattern.compile("-?[0-9]?(\\.[0-9]+)?"); //$NON-NLS-1$
private final static Pattern DIMENSION_PATTERN =
Pattern.compile("-?[0-9]+(\\.[0-9]+)?(dp|dip|sp|px|pt|in|mm)"); //$NON-NLS-1$
/**
* Checks the given value and returns true only if it is a valid XML value
* for this attribute.
*
* @param value the XML value to check
* @param projectResources project resources to validate resource URLs with,
* if any
* @param frameworkResources framework resources to validate resource URLs
* with, if any
* @return true if the value is valid, false otherwise
*/
public boolean isValid(
@NonNull String value,
@Nullable ResourceRepository projectResources,
@Nullable ResourceRepository frameworkResources) {
if (mFormats.contains(STRING) || mFormats.isEmpty()) {
// Anything is allowed
return true;
}
// All other formats require a nonempty string
if (value.isEmpty()) {
// Except for flags
if (mFormats.contains(FLAG)) {
return true;
}
return false;
}
char first = value.charAt(0);
// There are many attributes which are incorrectly marked in the attrs.xml
// file, such as "duration", "minHeight", etc. These are marked as only
// accepting "integer", but also appear to accept "reference". Therefore,
// in these cases, be more lenient. (This happens for theme references too,
// such as ?android:attr/listPreferredItemHeight)
if ((first == '@' || first == '?') /* && mFormats.contains(REFERENCE)*/) {
if (value.equals("@null")) {
return true;
}
if (value.startsWith(NEW_ID_PREFIX) || value.startsWith(ID_PREFIX)) {
// These are handled in the IdGeneratingResourceFile; we shouldn't
// complain about not finding ids in the repository yet since they may
// not yet have been defined (@+id's can be defined in the same layout,
// later on.)
return true;
}
if (value.startsWith(ANDROID_PREFIX) || value.startsWith(ANDROID_THEME_PREFIX)) {
if (frameworkResources != null) {
return frameworkResources.hasResourceItem(value);
}
} else if (projectResources != null) {
return projectResources.hasResourceItem(value);
}
// Validate resource string
String url = value;
int typeEnd = url.indexOf('/', 1);
if (typeEnd != -1) {
int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$
int colon = url.lastIndexOf(':', typeEnd);
if (colon != -1) {
typeBegin = colon + 1;
}
String typeName = url.substring(typeBegin, typeEnd);
ResourceType type = ResourceType.getEnum(typeName);
if (type != null) {
// TODO: Validate that the name portion conforms to the rules
// (is an identifier but not a keyword, etc.)
// Also validate that the prefix before the colon is either
// not there or is "android"
//int nameBegin = typeEnd + 1;
//String name = url.substring(nameBegin);
return true;
}
} else if (value.startsWith(PREFIX_THEME_REF)) {
if (projectResources != null) {
return projectResources.hasResourceItem(ResourceType.ATTR,
value.substring(PREFIX_THEME_REF.length()));
} else {
// Until proven otherwise
return true;
}
}
}
if (mFormats.contains(ENUM) && mEnumValues != null) {
for (String e : mEnumValues) {
if (value.equals(e)) {
return true;
}
}
}
if (mFormats.contains(FLAG) && mFlagValues != null) {
for (String v : Splitter.on('|').split(value)) {
for (String e : mFlagValues) {
if (v.equals(e)) {
return true;
}
}
}
}
if (mFormats.contains(DIMENSION)) {
if (DIMENSION_PATTERN.matcher(value).matches()) {
return true;
}
}
if (mFormats.contains(BOOLEAN)) {
if (value.equalsIgnoreCase(VALUE_TRUE) || value.equalsIgnoreCase(VALUE_FALSE)) {
return true;
}
}
if (mFormats.contains(FLOAT)) {
if (Character.isDigit(first) || first == '-' || first == '.') {
if (FLOAT_PATTERN.matcher(value).matches()) {
return true;
}
// AAPT accepts more general floats, such as ".1",
try {
Float.parseFloat(value);
return true;
} catch (NumberFormatException nufe) {
// Not a float
}
}
}
if (mFormats.contains(INTEGER)) {
if (Character.isDigit(first) || first == '-') {
if (INTEGER_PATTERN.matcher(value).matches()) {
return true;
}
}
}
if (mFormats.contains(COLOR)) {
if (first == '#' && value.length() <= 9) { // Only allowed 32 bit ARGB
try {
// Use Long.parseLong rather than Integer.parseInt to not overflow on
// 32 big hex values like "ff191919"
Long.parseLong(value.substring(1), 16);
return true;
} catch (NumberFormatException nufe) {
// Not a valid color number
}
}
}
if (mFormats.contains(FRACTION)) {
// should end with % or %p
return true;
}
return false;
}
}