diff options
Diffstat (limited to 'sdk_common/src')
5 files changed, 202 insertions, 31 deletions
diff --git a/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java b/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java index 1de664e..66a72ce 100644 --- a/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java +++ b/sdk_common/src/com/android/ide/common/resources/IdResourceParser.java @@ -16,6 +16,7 @@ package com.android.ide.common.resources; +import com.android.annotations.NonNull; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository; import com.android.resources.ResourceType; @@ -48,7 +49,9 @@ public class IdResourceParser { * as a place to stash errors encountered * @param isFramework true if scanning a framework resource */ - public IdResourceParser(IValueResourceRepository repository, ScanningContext context, + public IdResourceParser( + @NonNull IValueResourceRepository repository, + @NonNull ScanningContext context, boolean isFramework) { mRepository = repository; mContext = context; @@ -107,7 +110,6 @@ public class IdResourceParser { private boolean parse(ResourceType type, String path, KXmlParser parser) throws XmlPullParserException, IOException { boolean valid = true; - ResourceRepository resources = mContext.getRepository(); boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt(); while (true) { @@ -118,33 +120,22 @@ public class IdResourceParser { String value = parser.getAttributeValue(i); assert value != null : attribute; - if (value.startsWith("@")) { //$NON-NLS-1$ - // Gather IDs - if (value.startsWith("@+")) { //$NON-NLS-1$ - // Strip out the @+id/ or @+android:id/ section - String id = value.substring(value.indexOf('/') + 1); - ResourceValue newId = new ResourceValue(ResourceType.ID, id, - mIsFramework); - mRepository.addResourceValue(newId); - } else if (checkForErrors){ - // Validate resource references (unless we're scanning a framework - // resource or if we've already scheduled a full aapt run) - boolean exists = resources.hasResourceItem(value); - if (!exists && !mRepository.hasResourceValue(ResourceType.ID, - value.substring(value.indexOf('/') + 1))) { - String error = String.format( - // Don't localize because the exact pattern matches AAPT's - // output which has hardcoded regexp matching in - // AaptParser. - "%1$s:%2$d: Error: No resource found that matches " + //$NON-NLS-1$ - "the given name (at '%3$s' with value '%4$s')", //$NON-NLS-1$ - path, parser.getLineNumber(), - attribute, value); - mContext.addError(error); - valid = false; - } + if (checkForErrors) { + String uri = parser.getAttributeNamespace(i); + if (!mContext.checkValue(uri, attribute, value)) { + mContext.requestFullAapt(); + checkForErrors = false; + valid = false; } } + + if (value.startsWith("@+")) { //$NON-NLS-1$ + // Strip out the @+id/ or @+android:id/ section + String id = value.substring(value.indexOf('/') + 1); + ResourceValue newId = new ResourceValue(ResourceType.ID, id, + mIsFramework); + mRepository.addResourceValue(newId); + } } } else if (event == XmlPullParser.END_DOCUMENT) { break; diff --git a/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java b/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java index ac0614d..4f50f63 100755 --- a/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java +++ b/sdk_common/src/com/android/ide/common/resources/ResourceRepository.java @@ -171,7 +171,7 @@ public abstract class ResourceRepository { * @return true if the resource is known */ public boolean hasResourceItem(@NonNull String url) { - assert url.startsWith("@") : url; + assert url.startsWith("@") || url.startsWith("?") : url; int typeEnd = url.indexOf('/', 1); if (typeEnd != -1) { diff --git a/sdk_common/src/com/android/ide/common/resources/ScanningContext.java b/sdk_common/src/com/android/ide/common/resources/ScanningContext.java index e4ed275..43561e8 100644 --- a/sdk_common/src/com/android/ide/common/resources/ScanningContext.java +++ b/sdk_common/src/com/android/ide/common/resources/ScanningContext.java @@ -15,6 +15,9 @@ */ package com.android.ide.common.resources; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + import java.util.ArrayList; import java.util.List; @@ -24,7 +27,7 @@ import java.util.List; * so on. */ public class ScanningContext { - private final ResourceRepository mRepository; + protected final ResourceRepository mRepository; private boolean mNeedsFullAapt; private List<String> mErrors = null; @@ -33,7 +36,7 @@ public class ScanningContext { * * @param repository the associated resource repository */ - public ScanningContext(ResourceRepository repository) { + public ScanningContext(@NonNull ResourceRepository repository) { super(); mRepository = repository; } @@ -43,6 +46,7 @@ public class ScanningContext { * * @return a list of errors encountered during scanning (or null) */ + @Nullable public List<String> getErrors() { return mErrors; } @@ -55,7 +59,7 @@ public class ScanningContext { * @param error the error message, including file name and line number at * the beginning */ - public void addError(String error) { + public void addError(@NonNull String error) { if (mErrors == null) { mErrors = new ArrayList<String>(); } @@ -67,6 +71,7 @@ public class ScanningContext { * * @return the associated repository, never null */ + @NonNull public ResourceRepository getRepository() { return mRepository; } @@ -89,4 +94,17 @@ public class ScanningContext { public boolean needsFullAapt() { return mNeedsFullAapt; } + + /** + * Asks the context to check whether the given attribute name and value is valid + * in this context. + * + * @param uri the XML namespace URI + * @param name the attribute local name + * @param value the attribute value + * @return true if the attribute is valid + */ + public boolean checkValue(@Nullable String uri, @NonNull String name, @NonNull String value) { + return true; + } } diff --git a/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java b/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java index 6b663e9..eb643f6 100644 --- a/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java +++ b/sdk_common/src/com/android/ide/common/resources/SingleResourceFile.java @@ -88,6 +88,10 @@ public class SingleResourceFile extends ResourceFile { protected void update(ScanningContext context) { // when this happens, nothing needs to be done since the file only generates // a single resources that doesn't actually change (its content is the file path) + + // However, we should check for newly introduced errors + // Parse the file and look for @+id/ entries + validateAttributes(context); } @Override @@ -139,4 +143,26 @@ public class SingleResourceFile extends ResourceFile { return name; } + + /** + * Validates the associated resource file to make sure the attribute references are valid + * + * @return true if parsing succeeds and false if it fails + */ + private boolean validateAttributes(ScanningContext context) { + // We only need to check if it's a non-framework file + if (!isFramework()) { + ValidatingResourceParser parser = new ValidatingResourceParser(context, false); + try { + IAbstractFile file = getFile(); + return parser.parse(file.getOsLocation(), file.getContents()); + } catch (Exception e) { + context.needsFullAapt(); + } + + return false; + } + + return true; + } } diff --git a/sdk_common/src/com/android/ide/common/resources/ValidatingResourceParser.java b/sdk_common/src/com/android/ide/common/resources/ValidatingResourceParser.java new file mode 100644 index 0000000..c1e45a8 --- /dev/null +++ b/sdk_common/src/com/android/ide/common/resources/ValidatingResourceParser.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2011 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.ide.common.resources; + +import com.android.annotations.NonNull; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Parser for scanning an XML resource file and validating all framework + * attribute references in it. If an error is found, the associated context + * is marked as needing a full AAPT run. + */ +public class ValidatingResourceParser { + private final boolean mIsFramework; + private ScanningContext mContext; + + /** + * Creates a new {@link ValidatingResourceParser} + * + * @param context a context object with state for the current update, such + * as a place to stash errors encountered + * @param isFramework true if scanning a framework resource + */ + public ValidatingResourceParser( + @NonNull ScanningContext context, + boolean isFramework) { + mContext = context; + mIsFramework = isFramework; + } + + /** + * Parse the given input and return false if it contains errors, <b>or</b> if + * the context is already tagged as needing a full aapt run. + * + * @param path the full OS path to the file being parsed + * @param input the input stream of the XML to be parsed + * @return true if parsing succeeds and false if it fails + * @throws IOException if reading the contents fails + */ + public boolean parse(final String path, InputStream input) + throws IOException { + // No need to validate framework files + if (mIsFramework) { + return true; + } + if (mContext.needsFullAapt()) { + return false; + } + + KXmlParser parser = new KXmlParser(); + try { + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + + if (input instanceof FileInputStream) { + input = new BufferedInputStream(input); + } + parser.setInput(input, "UTF-8"); //$NON-NLS-1$ + + return parse(path, parser); + } catch (XmlPullParserException e) { + String message = e.getMessage(); + + // Strip off position description + int index = message.indexOf("(position:"); //$NON-NLS-1$ (Hardcoded in KXml) + if (index != -1) { + message = message.substring(0, index); + } + + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } catch (RuntimeException e) { + // Some exceptions are thrown by the KXmlParser that are not XmlPullParserExceptions, + // such as this one: + // java.lang.RuntimeException: Undefined Prefix: w in org.kxml2.io.KXmlParser@... + // at org.kxml2.io.KXmlParser.adjustNsp(Unknown Source) + // at org.kxml2.io.KXmlParser.parseStartTag(Unknown Source) + String message = e.getMessage(); + String error = String.format("%1$s:%2$d: Error: %3$s", //$NON-NLS-1$ + path, parser.getLineNumber(), message); + mContext.addError(error); + return false; + } + } + + private boolean parse(String path, KXmlParser parser) + throws XmlPullParserException, IOException { + boolean checkForErrors = !mIsFramework && !mContext.needsFullAapt(); + + while (true) { + int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + for (int i = 0, n = parser.getAttributeCount(); i < n; i++) { + String attribute = parser.getAttributeName(i); + String value = parser.getAttributeValue(i); + assert value != null : attribute; + + if (checkForErrors) { + String uri = parser.getAttributeNamespace(i); + if (!mContext.checkValue(uri, attribute, value)) { + mContext.requestFullAapt(); + return false; + } + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + + return true; + } +} |