1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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.ide.common.resources.platform;
18 
19 import static com.android.SdkConstants.ANDROID_PREFIX;
20 import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
21 import static com.android.SdkConstants.ID_PREFIX;
22 import static com.android.SdkConstants.NEW_ID_PREFIX;
23 import static com.android.SdkConstants.PREFIX_THEME_REF;
24 import static com.android.SdkConstants.VALUE_FALSE;
25 import static com.android.SdkConstants.VALUE_TRUE;
26 import static com.android.ide.common.api.IAttributeInfo.Format.BOOLEAN;
27 import static com.android.ide.common.api.IAttributeInfo.Format.COLOR;
28 import static com.android.ide.common.api.IAttributeInfo.Format.DIMENSION;
29 import static com.android.ide.common.api.IAttributeInfo.Format.ENUM;
30 import static com.android.ide.common.api.IAttributeInfo.Format.FLAG;
31 import static com.android.ide.common.api.IAttributeInfo.Format.FLOAT;
32 import static com.android.ide.common.api.IAttributeInfo.Format.FRACTION;
33 import static com.android.ide.common.api.IAttributeInfo.Format.INTEGER;
34 import static com.android.ide.common.api.IAttributeInfo.Format.STRING;
35 
36 import com.android.annotations.NonNull;
37 import com.android.annotations.Nullable;
38 import com.android.ide.common.api.IAttributeInfo;
39 import com.android.ide.common.resources.ResourceRepository;
40 import com.android.resources.ResourceType;
41 import com.google.common.base.Splitter;
42 
43 import java.util.EnumSet;
44 import java.util.regex.Pattern;
45 
46 
47 /**
48  * Information about an attribute as gathered from the attrs.xml file where
49  * the attribute was declared. This must include a format (string, reference, float, etc.),
50  * possible flag or enum values, whether it's deprecated and its javadoc.
51  */
52 public class AttributeInfo implements IAttributeInfo {
53     /** XML Name of the attribute */
54     private String mName;
55 
56     /** Formats of the attribute. Cannot be null. Should have at least one format. */
57     private EnumSet<Format> mFormats;
58     /** Values for enum. null for other types. */
59     private String[] mEnumValues;
60     /** Values for flag. null for other types. */
61     private String[] mFlagValues;
62     /** Short javadoc (i.e. the first sentence). */
63     private String mJavaDoc;
64     /** Documentation for deprecated attributes. Null if not deprecated. */
65     private String mDeprecatedDoc;
66     /** The source class defining this attribute */
67     private String mDefinedBy;
68 
69     /**
70      * @param name The XML Name of the attribute
71      * @param formats The formats of the attribute. Cannot be null.
72      *                Should have at least one format.
73      */
AttributeInfo(@onNull String name, @NonNull EnumSet<Format> formats)74     public AttributeInfo(@NonNull String name, @NonNull EnumSet<Format> formats) {
75         mName = name;
76         mFormats = formats;
77     }
78 
79     /**
80      * @param name The XML Name of the attribute
81      * @param formats The formats of the attribute. Cannot be null.
82      *                Should have at least one format.
83      * @param javadoc Short javadoc (i.e. the first sentence).
84      */
AttributeInfo(@onNull String name, @NonNull EnumSet<Format> formats, String javadoc)85     public AttributeInfo(@NonNull String name, @NonNull EnumSet<Format> formats, String javadoc) {
86         mName = name;
87         mFormats = formats;
88         mJavaDoc = javadoc;
89     }
90 
AttributeInfo(AttributeInfo info)91     public AttributeInfo(AttributeInfo info) {
92         mName = info.mName;
93         mFormats = info.mFormats;
94         mEnumValues = info.mEnumValues;
95         mFlagValues = info.mFlagValues;
96         mJavaDoc = info.mJavaDoc;
97         mDeprecatedDoc = info.mDeprecatedDoc;
98     }
99 
100     /**
101      * Sets the XML Name of the attribute
102      *
103      * @param name the new name to assign
104      */
setName(String name)105     public void setName(String name) {
106         mName = name;
107     }
108 
109     /** Returns the XML Name of the attribute */
110     @Override
getName()111     public @NonNull String getName() {
112         return mName;
113     }
114     /** Returns the formats of the attribute. Cannot be null.
115      *  Should have at least one format. */
116     @Override
getFormats()117     public @NonNull EnumSet<Format> getFormats() {
118         return mFormats;
119     }
120     /** Returns the values for enums. null for other types. */
121     @Override
getEnumValues()122     public String[] getEnumValues() {
123         return mEnumValues;
124     }
125     /** Returns the values for flags. null for other types. */
126     @Override
getFlagValues()127     public String[] getFlagValues() {
128         return mFlagValues;
129     }
130     /** Returns a short javadoc, .i.e. the first sentence. */
131     @Override
getJavaDoc()132     public @NonNull String getJavaDoc() {
133         return mJavaDoc;
134     }
135     /** Returns the documentation for deprecated attributes. Null if not deprecated. */
136     @Override
getDeprecatedDoc()137     public String getDeprecatedDoc() {
138         return mDeprecatedDoc;
139     }
140 
141     /** Sets the values for enums. null for other types. */
setEnumValues(String[] values)142     public AttributeInfo setEnumValues(String[] values) {
143         mEnumValues = values;
144         return this;
145     }
146 
147     /** Sets the values for flags. null for other types. */
setFlagValues(String[] values)148     public AttributeInfo setFlagValues(String[] values) {
149         mFlagValues = values;
150         return this;
151     }
152 
153     /** Sets a short javadoc, .i.e. the first sentence. */
setJavaDoc(String javaDoc)154     public void setJavaDoc(String javaDoc) {
155         mJavaDoc = javaDoc;
156     }
157 
158     /** Sets the documentation for deprecated attributes. Null if not deprecated. */
setDeprecatedDoc(String deprecatedDoc)159     public void setDeprecatedDoc(String deprecatedDoc) {
160         mDeprecatedDoc = deprecatedDoc;
161     }
162 
163     /**
164      * Sets the name of the class (fully qualified class name) which defined
165      * this attribute
166      *
167      * @param definedBy the name of the class (fully qualified class name) which
168      *            defined this attribute
169      */
setDefinedBy(String definedBy)170     public void setDefinedBy(String definedBy) {
171         mDefinedBy = definedBy;
172     }
173 
174     /**
175      * Returns the name of the class (fully qualified class name) which defined
176      * this attribute
177      *
178      * @return the name of the class (fully qualified class name) which defined
179      *         this attribute
180      */
181     @Override
getDefinedBy()182     public @NonNull String getDefinedBy() {
183         return mDefinedBy;
184     }
185 
186     private final static Pattern INTEGER_PATTERN = Pattern.compile("-?[0-9]+"); //$NON-NLS-1$
187     private final static Pattern FLOAT_PATTERN =
188             Pattern.compile("-?[0-9]?(\\.[0-9]+)?"); //$NON-NLS-1$
189     private final static Pattern DIMENSION_PATTERN =
190             Pattern.compile("-?[0-9]+(\\.[0-9]+)?(dp|dip|sp|px|pt|in|mm)"); //$NON-NLS-1$
191 
192     /**
193      * Checks the given value and returns true only if it is a valid XML value
194      * for this attribute.
195      *
196      * @param value the XML value to check
197      * @param projectResources project resources to validate resource URLs with,
198      *            if any
199      * @param frameworkResources framework resources to validate resource URLs
200      *            with, if any
201      * @return true if the value is valid, false otherwise
202      */
isValid( @onNull String value, @Nullable ResourceRepository projectResources, @Nullable ResourceRepository frameworkResources)203     public boolean isValid(
204             @NonNull String value,
205             @Nullable ResourceRepository projectResources,
206             @Nullable ResourceRepository frameworkResources) {
207 
208         if (mFormats.contains(STRING) || mFormats.isEmpty()) {
209             // Anything is allowed
210             return true;
211         }
212 
213         // All other formats require a nonempty string
214         if (value.isEmpty()) {
215             // Except for flags
216             if (mFormats.contains(FLAG)) {
217                 return true;
218             }
219 
220             return false;
221         }
222         char first = value.charAt(0);
223 
224         // There are many attributes which are incorrectly marked in the attrs.xml
225         // file, such as "duration", "minHeight", etc. These are marked as only
226         // accepting "integer", but also appear to accept "reference". Therefore,
227         // in these cases, be more lenient. (This happens for theme references too,
228         // such as ?android:attr/listPreferredItemHeight)
229         if ((first == '@' || first == '?') /* && mFormats.contains(REFERENCE)*/) {
230             if (value.equals("@null")) {
231                 return true;
232             }
233 
234             if (value.startsWith(NEW_ID_PREFIX) || value.startsWith(ID_PREFIX)) {
235                 // These are handled in the IdGeneratingResourceFile; we shouldn't
236                 // complain about not finding ids in the repository yet since they may
237                 // not yet have been defined (@+id's can be defined in the same layout,
238                 // later on.)
239                 return true;
240             }
241 
242             if (value.startsWith(ANDROID_PREFIX) || value.startsWith(ANDROID_THEME_PREFIX)) {
243                 if (frameworkResources != null) {
244                     return frameworkResources.hasResourceItem(value);
245                 }
246             } else if (projectResources != null) {
247                 return projectResources.hasResourceItem(value);
248             }
249 
250             // Validate resource string
251             String url = value;
252             int typeEnd = url.indexOf('/', 1);
253             if (typeEnd != -1) {
254                 int typeBegin = url.startsWith("@+") ? 2 : 1; //$NON-NLS-1$
255                 int colon = url.lastIndexOf(':', typeEnd);
256                 if (colon != -1) {
257                     typeBegin = colon + 1;
258                 }
259                 String typeName = url.substring(typeBegin, typeEnd);
260                 ResourceType type = ResourceType.getEnum(typeName);
261                 if (type != null) {
262                     // TODO: Validate that the name portion conforms to the rules
263                     // (is an identifier but not a keyword, etc.)
264                     // Also validate that the prefix before the colon is either
265                     // not there or is "android"
266 
267                     //int nameBegin = typeEnd + 1;
268                     //String name = url.substring(nameBegin);
269                     return true;
270                 }
271             } else if (value.startsWith(PREFIX_THEME_REF)) {
272                 if (projectResources != null) {
273                     return projectResources.hasResourceItem(ResourceType.ATTR,
274                             value.substring(PREFIX_THEME_REF.length()));
275                 } else {
276                     // Until proven otherwise
277                     return true;
278                 }
279             }
280         }
281 
282         if (mFormats.contains(ENUM) && mEnumValues != null) {
283             for (String e : mEnumValues) {
284                 if (value.equals(e)) {
285                     return true;
286                 }
287             }
288         }
289 
290         if (mFormats.contains(FLAG) && mFlagValues != null) {
291             for (String v : Splitter.on('|').split(value)) {
292                 for (String e : mFlagValues) {
293                     if (v.equals(e)) {
294                         return true;
295                     }
296                 }
297             }
298         }
299 
300         if (mFormats.contains(DIMENSION)) {
301             if (DIMENSION_PATTERN.matcher(value).matches()) {
302                 return true;
303             }
304         }
305 
306         if (mFormats.contains(BOOLEAN)) {
307             if (value.equalsIgnoreCase(VALUE_TRUE) || value.equalsIgnoreCase(VALUE_FALSE)) {
308                 return true;
309             }
310         }
311 
312         if (mFormats.contains(FLOAT)) {
313             if (Character.isDigit(first) || first == '-' || first == '.') {
314                 if (FLOAT_PATTERN.matcher(value).matches()) {
315                     return true;
316                 }
317                 // AAPT accepts more general floats, such as ".1",
318                 try {
319                     Float.parseFloat(value);
320                     return true;
321                 } catch (NumberFormatException nufe) {
322                     // Not a float
323                 }
324             }
325         }
326 
327         if (mFormats.contains(INTEGER)) {
328             if (Character.isDigit(first) || first == '-') {
329                 if (INTEGER_PATTERN.matcher(value).matches()) {
330                     return true;
331                 }
332             }
333         }
334 
335         if (mFormats.contains(COLOR)) {
336             if (first == '#' && value.length() <= 9) { // Only allowed 32 bit ARGB
337                 try {
338                     // Use Long.parseLong rather than Integer.parseInt to not overflow on
339                     // 32 big hex values like "ff191919"
340                     Long.parseLong(value.substring(1), 16);
341                     return true;
342                 } catch (NumberFormatException nufe) {
343                     // Not a valid color number
344                 }
345             }
346         }
347 
348         if (mFormats.contains(FRACTION)) {
349             // should end with % or %p
350             return true;
351         }
352 
353         return false;
354     }
355 }
356