diff options
Diffstat (limited to 'ide_common/src')
8 files changed, 330 insertions, 103 deletions
diff --git a/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java b/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java index 6706715..e4b6730 100644 --- a/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java +++ b/ide_common/src/com/android/ide/common/resources/IdGeneratingResourceFile.java @@ -24,8 +24,6 @@ import com.android.io.IAbstractFile; import com.android.io.StreamException; import com.android.resources.ResourceType; -import org.xml.sax.SAXException; - import java.io.IOException; import java.util.Collection; import java.util.HashMap; @@ -33,10 +31,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - /** * Represents a resource file that also generates ID resources. * <p/> @@ -45,11 +39,6 @@ import javax.xml.parsers.SAXParserFactory; public final class IdGeneratingResourceFile extends ResourceFile implements IValueResourceRepository { - private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance(); - static { - sParserFactory.setNamespaceAware(true); - } - private final Map<String, ResourceValue> mIdResources = new HashMap<String, ResourceValue>(); @@ -75,44 +64,49 @@ public final class IdGeneratingResourceFile extends ResourceFile mFileName = getFileName(type); // Get the resource value of this file as a whole layout - mFileValue = getFileValue(file,folder); + mFileValue = getFileValue(file, folder); } @Override - protected void load() { + protected void load(ScanningContext context) { // Parse the file and look for @+id/ entries - parseFileForIds(); + parseFileForIds(context); // create the resource items in the repository - updateResourceItems(); + updateResourceItems(context); } @Override - protected void update() { + protected void update(ScanningContext context) { // Copy the previous list of ID names - Set<String> oldIdNames = mIdResources.keySet(); + Set<String> oldIdNames = new HashSet<String>(mIdResources.keySet()); // reset current content. mIdResources.clear(); // need to parse the file and find the IDs. - parseFileForIds(); + if (!parseFileForIds(context)) { + context.requestFullAapt(); + return; + } // We only need to update the repository if our IDs have changed - if (oldIdNames.equals(mIdResources.keySet()) == false) { - updateResourceItems(); + Set<String> keySet = mIdResources.keySet(); + assert keySet != oldIdNames; + if (oldIdNames.equals(keySet) == false) { + updateResourceItems(context); } } @Override - protected void dispose() { + protected void dispose(ScanningContext context) { ResourceRepository repository = getRepository(); // Remove declarations from this file from the repository repository.removeFile(mResourceTypeList, this); // Ask for an ID refresh since we'll be taking away ID generating items - repository.markForIdRefresh(); + context.requestFullAapt(); } @Override @@ -145,22 +139,27 @@ public final class IdGeneratingResourceFile extends ResourceFile /** * Looks through the file represented for Ids and adds them to * our id repository + * + * @return true if parsing succeeds and false if it fails */ - private void parseFileForIds() { + private boolean parseFileForIds(ScanningContext context) { + IdResourceParser parser = new IdResourceParser(this, context, isFramework()); try { - SAXParser parser = sParserFactory.newSAXParser(); - parser.parse(getFile().getContents(), new IdResourceParser(this, isFramework())); - } catch (ParserConfigurationException e) { - } catch (SAXException e) { + IAbstractFile file = getFile(); + return parser.parse(mFileType, file.getOsLocation(), file.getContents()); } catch (IOException e) { + // Pass } catch (StreamException e) { + // Pass } + + return false; } /** * Add the resources represented by this file to the repository */ - private void updateResourceItems() { + private void updateResourceItems(ScanningContext context) { ResourceRepository repository = getRepository(); // remove this file from all existing ResourceItem. @@ -178,7 +177,7 @@ public final class IdGeneratingResourceFile extends ResourceFile } // Ask the repository for an ID refresh - repository.markForIdRefresh(); + context.requestFullAapt(); } /** diff --git a/ide_common/src/com/android/ide/common/resources/IdResourceParser.java b/ide_common/src/com/android/ide/common/resources/IdResourceParser.java index 27195a9..324ad2b 100644 --- a/ide_common/src/com/android/ide/common/resources/IdResourceParser.java +++ b/ide_common/src/com/android/ide/common/resources/IdResourceParser.java @@ -20,35 +20,136 @@ import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ValueResourceParser.IValueResourceRepository; import com.android.resources.ResourceType; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; -public class IdResourceParser extends DefaultHandler { +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +/** + * Parser for scanning an id-generating resource file such as a layout or a menu + * file, which registers any ids it encounters with an + * {@link IValueResourceRepository}, and which registers errors with a + * {@link ScanningContext}. + */ +public class IdResourceParser { private final IValueResourceRepository mRepository; private final boolean mIsFramework; + private ScanningContext mContext; - public IdResourceParser(IValueResourceRepository repository, boolean isFramework) { - super(); + /** + * Creates a new {@link IdResourceParser} + * + * @param repository value repository for registering resource declaration + * @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 IdResourceParser(IValueResourceRepository repository, ScanningContext context, + boolean isFramework) { mRepository = repository; + mContext = context; mIsFramework = isFramework; } - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { - for (int i = 0; i < attributes.getLength(); ++i) { - // Let's look up the value to look for the @+*id/ pattern - String candidate = attributes.getValue(i); - // Right now the only things that start with the @+ pattern are IDs. If this changes - // in the future we'll have to change this line - if (candidate != null && candidate.startsWith("@+")) { - // Strip out the @+id/ or @+android:id/ section - String id = candidate.substring(candidate.indexOf('/') + 1); - ResourceValue newId = new ResourceValue(ResourceType.ID, id, mIsFramework); - mRepository.addResourceValue(newId); + /** + * Parse the given input and register ids with the given + * {@link IValueResourceRepository}. + * + * @param type the type of resource being scanned + * @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(ResourceType type, final String path, InputStream input) + throws IOException { + 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(type, 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(ResourceType type, String path, KXmlParser parser) + throws XmlPullParserException, IOException { + boolean valid = true; + ResourceRepository resources = mContext.getRepository(); + 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 (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) { + 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; + } + } + } + } + } else if (event == XmlPullParser.END_DOCUMENT) { + break; + } + } + + return valid; + } } diff --git a/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java b/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java index b3e35d9..b95a98c 100644 --- a/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java +++ b/ide_common/src/com/android/ide/common/resources/MultiResourceFile.java @@ -59,7 +59,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou private boolean mNeedIdRefresh; @Override - protected void load() { + protected void load(ScanningContext context) { // need to parse the file and find the content. parseFile(); @@ -70,11 +70,11 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou mNeedIdRefresh = true; // create/update the resource items. - updateResourceItems(); + updateResourceItems(context); } @Override - protected void update() { + protected void update(ScanningContext context) { // Reset the ID generation flag mNeedIdRefresh = false; @@ -107,18 +107,18 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou mNeedIdRefresh = true; } // create/update the resource items. - updateResourceItems(); + updateResourceItems(context); } @Override - protected void dispose() { + protected void dispose(ScanningContext context) { ResourceRepository repository = getRepository(); // only remove this file from all existing ResourceItem. repository.removeFile(mResourceTypeList, this); // We'll need an ID refresh because we deleted items - repository.markForIdRefresh(); + context.requestFullAapt(); // don't need to touch the content, it'll get reclaimed as this objects disappear. // In the mean time other objects may need to access it. @@ -135,7 +135,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou return (list != null && list.size() > 0); } - private void updateResourceItems() { + private void updateResourceItems(ScanningContext context) { ResourceRepository repository = getRepository(); // remove this file from all existing ResourceItem. @@ -157,7 +157,7 @@ public final class MultiResourceFile extends ResourceFile implements IValueResou // If we need an ID refresh, ask the repository for that now if (mNeedIdRefresh) { - repository.markForIdRefresh(); + context.requestFullAapt(); } } diff --git a/ide_common/src/com/android/ide/common/resources/ResourceFile.java b/ide_common/src/com/android/ide/common/resources/ResourceFile.java index 03f0b34..bc5b750 100644 --- a/ide_common/src/com/android/ide/common/resources/ResourceFile.java +++ b/ide_common/src/com/android/ide/common/resources/ResourceFile.java @@ -37,9 +37,9 @@ public abstract class ResourceFile implements Configurable { mFolder = folder; } - protected abstract void load(); - protected abstract void update(); - protected abstract void dispose(); + protected abstract void load(ScanningContext context); + protected abstract void update(ScanningContext context); + protected abstract void dispose(ScanningContext context); public FolderConfiguration getConfiguration() { return mFolder.getConfiguration(); diff --git a/ide_common/src/com/android/ide/common/resources/ResourceFolder.java b/ide_common/src/com/android/ide/common/resources/ResourceFolder.java index e55e14c..b8e0cda 100644 --- a/ide_common/src/com/android/ide/common/resources/ResourceFolder.java +++ b/ide_common/src/com/android/ide/common/resources/ResourceFolder.java @@ -47,7 +47,7 @@ public final class ResourceFolder implements Configurable { * @param type The type of the folder * @param config The configuration of the folder * @param folder The associated {@link IAbstractFolder} object. - * @param isFrameworkRepository + * @param repository The associated {@link ResourceRepository} */ protected ResourceFolder(ResourceFolderType type, FolderConfiguration config, IAbstractFolder folder, ResourceRepository repository) { @@ -59,12 +59,15 @@ public final class ResourceFolder implements Configurable { /** * Processes a file and adds it to its parent folder resource. + * * @param file the underlying resource file. - * @param folder the parent of the resource file. * @param kind the file change kind. + * @param context a context object with state for the current update, such + * as a place to stash errors encountered * @return the {@link ResourceFile} that was created. */ - public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind) { + public ResourceFile processFile(IAbstractFile file, ResourceDeltaKind kind, + ScanningContext context) { // look for this file if it's already been created ResourceFile resFile = getFile(file); @@ -84,7 +87,7 @@ public final class ResourceFolder implements Configurable { if (types.size() == 1) { resFile = new SingleResourceFile(file, this); - } else if (types.contains(ResourceType.LAYOUT)){ + } else if (types.contains(ResourceType.LAYOUT)) { resFile = new IdGeneratingResourceFile(file, this, ResourceType.LAYOUT); } else if (types.contains(ResourceType.MENU)) { resFile = new IdGeneratingResourceFile(file, this, ResourceType.MENU); @@ -92,16 +95,16 @@ public final class ResourceFolder implements Configurable { resFile = new MultiResourceFile(file, this); } - resFile.load(); + resFile.load(context); // add it to the folder addFile(resFile); } } else { if (kind == ResourceDeltaKind.REMOVED) { - removeFile(resFile); + removeFile(resFile, context); } else { - resFile.update(); + resFile.update(context); } } @@ -122,15 +125,15 @@ public final class ResourceFolder implements Configurable { mFiles.add(file); } - protected void removeFile(ResourceFile file) { - file.dispose(); + protected void removeFile(ResourceFile file, ScanningContext context) { + file.dispose(context); mFiles.remove(file); } - protected void dispose() { + protected void dispose(ScanningContext context) { if (mFiles != null) { for (ResourceFile file : mFiles) { - file.dispose(); + file.dispose(context); } mFiles.clear(); diff --git a/ide_common/src/com/android/ide/common/resources/ResourceRepository.java b/ide_common/src/com/android/ide/common/resources/ResourceRepository.java index 4af4a1a..d78f1d1 100644 --- a/ide_common/src/com/android/ide/common/resources/ResourceRepository.java +++ b/ide_common/src/com/android/ide/common/resources/ResourceRepository.java @@ -68,8 +68,6 @@ public abstract class ResourceRepository { protected final IntArrayWrapper mWrapper = new IntArrayWrapper(null); - private boolean mNeedsIdRefresh; - /** * Makes a resource repository * @param isFrameworkRepository whether the repository is for framework resources. @@ -129,7 +127,8 @@ public abstract class ResourceRepository { * @param removedFolder the IAbstractFolder object. * @return the {@link ResourceFolder} that was removed, or null if no matches were found. */ - public ResourceFolder removeFolder(ResourceFolderType type, IAbstractFolder removedFolder) { + public ResourceFolder removeFolder(ResourceFolderType type, IAbstractFolder removedFolder, + ScanningContext context) { // get the list of folders for the resource type. List<ResourceFolder> list = mFolderMap.get(type); @@ -143,7 +142,7 @@ public abstract class ResourceRepository { list.remove(i); // remove its content - resFolder.dispose(); + resFolder.dispose(context); return resFolder; } @@ -154,6 +153,60 @@ public abstract class ResourceRepository { } /** + * Returns true if this resource repository contains a resource of the given + * name. + * + * @param url the resource URL + * @return true if the resource is known + */ + public boolean hasResourceItem(String url) { + assert url.startsWith("@") : url; + + int typeEnd = url.indexOf('/', 1); + if (typeEnd != -1) { + int nameBegin = typeEnd + 1; + + // Skip @ and @+ + 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) { + String name = url.substring(nameBegin); + return hasResourceItem(type, name); + } + } + + return false; + } + + /** + * Returns true if this resource repository contains a resource of the given + * name. + * + * @param type the type of resource to look up + * @param name the name of the resource + * @return true if the resource is known + */ + public boolean hasResourceItem(ResourceType type, String name) { + List<ResourceItem> list = mResourceMap.get(type); + + if (list != null) { + for (ResourceItem item : list) { + if (name.equals(item.getName())) { + return true; + } + } + } + + return false; + } + + /** * Returns a {@link ResourceItem} matching the given {@link ResourceType} and name. If none * exist, it creates one. * @@ -196,29 +249,6 @@ public abstract class ResourceRepository { protected abstract ResourceItem createResourceItem(String name); /** - * Sets a flag which determines whether aapt needs to be run to regenerate resource IDs - */ - protected void markForIdRefresh() { - mNeedsIdRefresh = true; - } - - /** - * Returns whether this repository has been marked as "dirty"; if one or more of the constituent - * files have declared that the resource item names that they provide have changed. - */ - public boolean needsIdRefresh() { - return mNeedsIdRefresh; - } - - /** - * Indicates that the resources IDs have been regenerated, so the repository is now in a clean - * state - */ - public void setIdsRefreshed() { - mNeedsIdRefresh = false; - } - - /** * Processes a folder and adds it to the list of existing folders. * @param folder the folder to process * @return the ResourceFolder created from this folder, or null if the process failed. @@ -496,6 +526,8 @@ public abstract class ResourceRepository { */ public void loadResources(IAbstractFolder rootFolder) throws IOException { + ScanningContext context = new ScanningContext(this); + IAbstractResource[] files = rootFolder.listMembers(); for (IAbstractResource file : files) { if (file instanceof IAbstractFolder) { @@ -509,7 +541,7 @@ public abstract class ResourceRepository { for (IAbstractResource childRes : children) { if (childRes instanceof IAbstractFile) { resFolder.processFile((IAbstractFile) childRes, - ResourceDeltaKind.ADDED); + ResourceDeltaKind.ADDED, context); } } } diff --git a/ide_common/src/com/android/ide/common/resources/ScanningContext.java b/ide_common/src/com/android/ide/common/resources/ScanningContext.java new file mode 100644 index 0000000..e4ed275 --- /dev/null +++ b/ide_common/src/com/android/ide/common/resources/ScanningContext.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2011 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; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link ScanningContext} keeps track of state during a resource file scan, + * such as any parsing errors encountered, whether Android ids have changed, and + * so on. + */ +public class ScanningContext { + private final ResourceRepository mRepository; + private boolean mNeedsFullAapt; + private List<String> mErrors = null; + + /** + * Constructs a new {@link ScanningContext} + * + * @param repository the associated resource repository + */ + public ScanningContext(ResourceRepository repository) { + super(); + mRepository = repository; + } + + /** + * Returns a list of errors encountered during scanning + * + * @return a list of errors encountered during scanning (or null) + */ + public List<String> getErrors() { + return mErrors; + } + + /** + * Adds the given error to the scanning context. The error should use the + * same syntax as real aapt error messages such that the aapt parser can + * properly detect the filename, line number, etc. + * + * @param error the error message, including file name and line number at + * the beginning + */ + public void addError(String error) { + if (mErrors == null) { + mErrors = new ArrayList<String>(); + } + mErrors.add(error); + } + + /** + * Returns the repository associated with this scanning context + * + * @return the associated repository, never null + */ + public ResourceRepository getRepository() { + return mRepository; + } + + /** + * Marks that a full aapt compilation of the resources is necessary because it has + * detected a change that cannot be incrementally handled. + */ + protected void requestFullAapt() { + mNeedsFullAapt = true; + } + + /** + * Returns whether this repository has been marked as "dirty"; if one or + * more of the constituent files have declared that the resource item names + * that they provide have changed. + * + * @return true if a full aapt compilation is required + */ + public boolean needsFullAapt() { + return mNeedsFullAapt; + } +} diff --git a/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java b/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java index b589b35..6b663e9 100644 --- a/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java +++ b/ide_common/src/com/android/ide/common/resources/SingleResourceFile.java @@ -73,7 +73,7 @@ public class SingleResourceFile extends ResourceFile { } @Override - protected void load() { + protected void load(ScanningContext context) { // get a resource item matching the given type and name ResourceItem item = getRepository().getResourceItem(mType, mResourceName); @@ -81,22 +81,22 @@ public class SingleResourceFile extends ResourceFile { item.add(this); // Ask for an ID refresh since we're adding an item that will generate an ID - getRepository().markForIdRefresh(); + context.requestFullAapt(); } @Override - protected void update() { + 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) } @Override - protected void dispose() { + protected void dispose(ScanningContext context) { // only remove this file from the existing ResourceItem. getFolder().getRepository().removeFile(mType, this); // Ask for an ID refresh since we're removing an item that previously generated an ID - getRepository().markForIdRefresh(); + context.requestFullAapt(); // don't need to touch the content, it'll get reclaimed as this objects disappear. // In the mean time other objects may need to access it. |