2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.resources.manager;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.AndroidConstants;
21 import com.android.ide.eclipse.adt.internal.resources.ResourceType;
22 import com.android.ide.eclipse.adt.internal.resources.configurations.FolderConfiguration;
23 import com.android.ide.eclipse.adt.internal.resources.configurations.ResourceQualifier;
24 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
25 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFolderListener;
26 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
27 import com.android.ide.eclipse.adt.io.IFileWrapper;
28 import com.android.ide.eclipse.adt.io.IFolderWrapper;
29 import com.android.sdklib.IAndroidTarget;
30 import com.android.sdklib.SdkConstants;
31 import com.android.sdklib.io.FolderWrapper;
32 import com.android.sdklib.io.IAbstractFile;
33 import com.android.sdklib.io.IAbstractFolder;
34 import com.android.sdklib.io.IAbstractResource;
36 import org.eclipse.core.resources.IContainer;
37 import org.eclipse.core.resources.IFile;
38 import org.eclipse.core.resources.IFolder;
39 import org.eclipse.core.resources.IMarkerDelta;
40 import org.eclipse.core.resources.IProject;
41 import org.eclipse.core.resources.IResource;
42 import org.eclipse.core.resources.IResourceDelta;
43 import org.eclipse.core.runtime.CoreException;
44 import org.eclipse.core.runtime.IPath;
47 import java.io.IOException;
48 import java.util.ArrayList;
49 import java.util.HashMap;
52 * The ResourceManager tracks resources for all opened projects.
54 * It provide direct access to all the resources of a project as a {@link ProjectResources}
55 * object that allows accessing the resources through their file representation or as Android
56 * resources (similar to what is seen by an Android application).
58 * The ResourceManager automatically tracks file changes to update its internal representation
59 * of the resources so that they are always up to date.
61 * It also gives access to a monitor that is more resource oriented than the
62 * {@link GlobalProjectMonitor}.
63 * This monitor will let you track resource changes by giving you direct access to
64 * {@link ResourceFile}, or {@link ResourceFolder}.
66 * @see ProjectResources
68 public final class ResourceManager {
70 private final static ResourceManager sThis = new ResourceManager();
72 /** List of the qualifier object helping for the parsing of folder names */
73 private final ResourceQualifier[] mQualifiers;
76 * Map associating project resource with project objects.
77 * <p/><b>All accesses must be inside a synchronized(mMap) block</b>, and do as a little as
78 * possible and <b>not call out to other classes</b>.
80 private final HashMap<IProject, ProjectResources> mMap =
81 new HashMap<IProject, ProjectResources>();
84 * Interface to be notified of resource changes.
86 * @see ResourceManager#addListener(IResourceListener)
87 * @see ResourceManager#removeListener(IResource)
89 public interface IResourceListener {
91 * Notification for resource file change.
92 * @param project the project of the file.
93 * @param file the {@link ResourceFile} representing the file.
94 * @param eventType the type of event. See {@link IResourceDelta}.
96 void fileChanged(IProject project, ResourceFile file, int eventType);
98 * Notification for resource folder change.
99 * @param project the project of the file.
100 * @param folder the {@link ResourceFolder} representing the folder.
101 * @param eventType the type of event. See {@link IResourceDelta}.
103 void folderChanged(IProject project, ResourceFolder folder, int eventType);
106 private final ArrayList<IResourceListener> mListeners = new ArrayList<IResourceListener>();
109 * Sets up the resource manager with the global project monitor.
110 * @param monitor The global project monitor
112 public static void setup(GlobalProjectMonitor monitor) {
113 monitor.addProjectListener(sThis.mProjectListener);
114 int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED;
115 monitor.addFolderListener(sThis.mFolderListener, mask);
116 monitor.addFileListener(sThis.mFileListener, mask);
118 CompiledResourcesMonitor.setupMonitor(monitor);
122 * Returns the singleton instance.
124 public static ResourceManager getInstance() {
129 * Adds a new {@link IResourceListener} to be notified of resource changes.
130 * @param listener the listener to be added.
132 public void addListener(IResourceListener listener) {
133 synchronized (mListeners) {
134 mListeners.add(listener);
139 * Removes an {@link IResourceListener}, so that it's not notified of resource changes anymore.
140 * @param listener the listener to be removed.
142 public void removeListener(IResource listener) {
143 synchronized (mListeners) {
144 mListeners.remove(listener);
149 * Returns the resources of a project.
150 * @param project The project
151 * @return a ProjectResources object or null if none was found.
153 public ProjectResources getProjectResources(IProject project) {
154 synchronized (mMap) {
155 return mMap.get(project);
160 * Implementation of the {@link IFolderListener} as an internal class so that the methods
161 * do not appear in the public API of {@link ResourceManager}.
163 private IFolderListener mFolderListener = new IFolderListener() {
164 public void folderChanged(IFolder folder, int kind) {
165 ProjectResources resources;
167 final IProject project = folder.getProject();
170 if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
173 } catch (CoreException e) {
174 // can't get the project nature? return!
179 case IResourceDelta.ADDED:
180 // checks if the folder is under res.
181 IPath path = folder.getFullPath();
183 // the path will be project/res/<something>
184 if (path.segmentCount() == 3) {
185 if (isInResFolder(path)) {
186 // get the project and its resource object.
187 synchronized (mMap) {
188 resources = mMap.get(project);
190 // if it doesn't exist, we create it.
191 if (resources == null) {
192 resources = new ProjectResources(project);
193 mMap.put(project, resources);
197 ResourceFolder newFolder = processFolder(new IFolderWrapper(folder),
199 if (newFolder != null) {
200 notifyListenerOnFolderChange(project, newFolder, kind);
205 case IResourceDelta.CHANGED:
206 synchronized (mMap) {
207 resources = mMap.get(folder.getProject());
209 if (resources != null) {
210 ResourceFolder resFolder = resources.getResourceFolder(folder);
211 if (resFolder != null) {
213 notifyListenerOnFolderChange(project, resFolder, kind);
217 case IResourceDelta.REMOVED:
218 synchronized (mMap) {
219 resources = mMap.get(folder.getProject());
221 if (resources != null) {
222 // lets get the folder type
223 ResourceFolderType type = ResourceFolderType.getFolderType(
226 ResourceFolder removedFolder = resources.removeFolder(type, folder);
227 if (removedFolder != null) {
228 notifyListenerOnFolderChange(project, removedFolder, kind);
237 * Implementation of the {@link IFileListener} as an internal class so that the methods
238 * do not appear in the public API of {@link ResourceManager}.
240 private IFileListener mFileListener = new IFileListener() {
242 * Sent when a file changed. Depending on the file being changed, and the type of change
243 * (ADDED, REMOVED, CHANGED), the file change is processed to update the resource
246 * @param file The file that changed.
247 * @param markerDeltas The marker deltas for the file.
248 * @param kind The change kind. This is equivalent to
249 * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
251 * @see IFileListener#fileChanged
253 public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
254 ProjectResources resources;
256 final IProject project = file.getProject();
259 if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
262 } catch (CoreException e) {
263 // can't get the project nature? return!
268 case IResourceDelta.ADDED:
269 // checks if the file is under res/something.
270 IPath path = file.getFullPath();
272 if (path.segmentCount() == 4) {
273 if (isInResFolder(path)) {
274 // get the project and its resources
275 synchronized (mMap) {
276 resources = mMap.get(project);
279 IContainer container = file.getParent();
280 if (container instanceof IFolder && resources != null) {
282 ResourceFolder folder = resources.getResourceFolder(
285 if (folder != null) {
286 ResourceFile resFile = processFile(
287 new IFileWrapper(file), folder);
288 notifyListenerOnFileChange(project, resFile, kind);
294 case IResourceDelta.CHANGED:
295 // try to find a matching ResourceFile
296 synchronized (mMap) {
297 resources = mMap.get(project);
299 if (resources != null) {
300 IContainer container = file.getParent();
301 if (container instanceof IFolder) {
302 ResourceFolder resFolder = resources.getResourceFolder(
305 // we get the delete on the folder before the file, so it is possible
306 // the associated ResourceFolder doesn't exist anymore.
307 if (resFolder != null) {
308 // get the resourceFile, and touch it.
309 ResourceFile resFile = resFolder.getFile(file);
310 if (resFile != null) {
312 notifyListenerOnFileChange(project, resFile, kind);
318 case IResourceDelta.REMOVED:
319 // try to find a matching ResourceFile
320 synchronized (mMap) {
321 resources = mMap.get(project);
323 if (resources != null) {
324 IContainer container = file.getParent();
325 if (container instanceof IFolder) {
326 ResourceFolder resFolder = resources.getResourceFolder(
329 // we get the delete on the folder before the file, so it is possible
330 // the associated ResourceFolder doesn't exist anymore.
331 if (resFolder != null) {
333 ResourceFile resFile = resFolder.removeFile(file);
334 if (resFile != null) {
335 notifyListenerOnFileChange(project, resFile, kind);
347 * Implementation of the {@link IProjectListener} as an internal class so that the methods
348 * do not appear in the public API of {@link ResourceManager}.
350 private IProjectListener mProjectListener = new IProjectListener() {
351 public void projectClosed(IProject project) {
352 synchronized (mMap) {
353 mMap.remove(project);
357 public void projectDeleted(IProject project) {
358 synchronized (mMap) {
359 mMap.remove(project);
363 public void projectOpened(IProject project) {
364 createProject(project);
367 public void projectOpenedWithWorkspace(IProject project) {
368 createProject(project);
371 public void projectRenamed(IProject project, IPath from) {
372 // renamed project get a delete/open event too, so this can be ignored.
377 * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists.
379 public ResourceFolder getResourceFolder(IFile file) {
380 IContainer container = file.getParent();
381 if (container.getType() == IResource.FOLDER) {
382 IFolder parent = (IFolder)container;
383 IProject project = file.getProject();
385 ProjectResources resources = getProjectResources(project);
386 if (resources != null) {
387 return resources.getResourceFolder(parent);
395 * Returns the {@link ResourceFolder} for the given folder or <code>null</code> if none exists.
397 public ResourceFolder getResourceFolder(IFolder folder) {
398 IProject project = folder.getProject();
400 ProjectResources resources = getProjectResources(project);
401 if (resources != null) {
402 return resources.getResourceFolder(folder);
409 * Loads and returns the resources for a given {@link IAndroidTarget}
410 * @param androidTarget the target from which to load the framework resources
412 public ProjectResources loadFrameworkResources(IAndroidTarget androidTarget) {
413 String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES);
415 FolderWrapper frameworkRes = new FolderWrapper(osResourcesPath);
416 if (frameworkRes.exists()) {
417 ProjectResources resources = new ProjectResources();
420 loadResources(resources, frameworkRes);
422 } catch (IOException e) {
423 // since we test that folders are folders, and files are files, this shouldn't
424 // happen. We can ignore it.
432 * Loads the resources from a folder, and fills the given {@link ProjectResources}.
434 * This is mostly a utility method that should not be used to process actual Eclipse projects
435 * (Those are loaded with {@link #createProject(IProject)} for new project or
436 * {@link #processFolder(IAbstractFolder, ProjectResources)} and
437 * {@link #processFile(IAbstractFile, ResourceFolder)} for folder/file modifications)<br>
438 * This method will process files/folders with implementations of {@link IAbstractFile} and
439 * {@link IAbstractFolder} based on {@link File} instead of {@link IFile} and {@link IFolder}
440 * respectively. This is not proper for handling {@link IProject}s.
442 * This is used to load the framework resources, or to do load project resources when
443 * setting rendering tests.
446 * @param resources The {@link ProjectResources} files to load. It is expected that the
447 * framework flag has been properly setup. This is filled up with the content of the folder.
448 * @param rootFolder The folder to read the resources from. This is the top level
449 * resource folder (res/)
450 * @throws IOException
452 public void loadResources(ProjectResources resources, IAbstractFolder rootFolder)
454 IAbstractResource[] files = rootFolder.listMembers();
455 for (IAbstractResource file : files) {
456 if (file instanceof IAbstractFolder) {
457 IAbstractFolder folder = (IAbstractFolder) file;
458 ResourceFolder resFolder = processFolder(folder, resources);
460 if (resFolder != null) {
461 // now we process the content of the folder
462 IAbstractResource[] children = folder.listMembers();
464 for (IAbstractResource childRes : children) {
465 if (childRes instanceof IAbstractFile) {
466 processFile((IAbstractFile) childRes, resFolder);
473 // now that we have loaded the files, we need to force load the resources from them
478 * Initial project parsing to gather resource info.
481 private void createProject(IProject project) {
482 if (project.isOpen()) {
484 if (project.hasNature(AndroidConstants.NATURE_DEFAULT) == false) {
487 } catch (CoreException e1) {
488 // can't check the nature of the project? ignore it.
492 IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES);
494 ProjectResources projectResources;
495 synchronized (mMap) {
496 projectResources = mMap.get(project);
497 if (projectResources == null) {
498 projectResources = new ProjectResources(project);
499 mMap.put(project, projectResources);
503 if (resourceFolder != null && resourceFolder.exists()) {
505 IResource[] resources = resourceFolder.members();
507 for (IResource res : resources) {
508 if (res.getType() == IResource.FOLDER) {
509 IFolder folder = (IFolder)res;
510 ResourceFolder resFolder = processFolder(new IFolderWrapper(folder),
513 if (resFolder != null) {
514 // now we process the content of the folder
515 IResource[] files = folder.members();
517 for (IResource fileRes : files) {
518 if (fileRes.getType() == IResource.FILE) {
519 IFile file = (IFile)fileRes;
521 processFile(new IFileWrapper(file), resFolder);
527 } catch (CoreException e) {
528 // This happens if the project is closed or if the folder doesn't exist.
529 // Since we already test for that, we can ignore this exception.
536 * Creates a {@link FolderConfiguration} matching the folder segments.
537 * @param folderSegments The segments of the folder name. The first segments should contain
538 * the name of the folder
539 * @return a FolderConfiguration object, or null if the folder name isn't valid..
541 public FolderConfiguration getConfig(String[] folderSegments) {
542 FolderConfiguration config = new FolderConfiguration();
544 // we are going to loop through the segments, and match them with the first
545 // available qualifier. If the segment doesn't match we try with the next qualifier.
546 // Because the order of the qualifier is fixed, we do not reset the first qualifier
547 // after each sucessful segment.
548 // If we run out of qualifier before processing all the segments, we fail.
550 int qualifierIndex = 0;
551 int qualifierCount = mQualifiers.length;
553 for (int i = 1 ; i < folderSegments.length; i++) {
554 String seg = folderSegments[i];
555 if (seg.length() > 0) {
556 while (qualifierIndex < qualifierCount &&
557 mQualifiers[qualifierIndex].checkAndSet(seg, config) == false) {
561 // if we reached the end of the qualifier we didn't find a matching qualifier.
562 if (qualifierIndex == qualifierCount) {
575 * Processes a folder and adds it to the list of the project resources.
576 * @param folder the folder to process
577 * @param project the folder's project.
578 * @return the ConfiguredFolder created from this folder, or null if the process failed.
580 private ResourceFolder processFolder(IAbstractFolder folder, ProjectResources project) {
581 // split the name of the folder in segments.
582 String[] folderSegments = folder.getName().split(FolderConfiguration.QUALIFIER_SEP);
584 // get the enum for the resource type.
585 ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]);
588 // get the folder configuration.
589 FolderConfiguration config = getConfig(folderSegments);
591 if (config != null) {
592 ResourceFolder configuredFolder = project.add(type, config, folder);
594 return configuredFolder;
602 * Processes a file and adds it to its parent folder resource.
603 * @param file the underlying resource file.
604 * @param folder the parent of the resource file.
605 * @return the {@link ResourceFile} that was created.
607 private ResourceFile processFile(IAbstractFile file, ResourceFolder folder) {
608 // get the type of the folder
609 ResourceFolderType type = folder.getType();
611 // look for this file if it's already been created
612 ResourceFile resFile = folder.getFile(file);
614 if (resFile != null) {
615 // invalidate the file
618 // create a ResourceFile for it.
620 // check if that's a single or multi resource type folder. For now we define this by
621 // the number of possible resource type output by files in the folder. This does
622 // not make the difference between several resource types from a single file or
623 // the ability to have 2 files in the same folder generating 2 different types of
624 // resource. The former is handled by MultiResourceFile properly while we don't
625 // handle the latter. If we were to add this behavior we'd have to change this call.
626 ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(type);
628 if (types.length == 1) {
629 resFile = new SingleResourceFile(file, folder);
631 resFile = new MultiResourceFile(file, folder);
634 // add it to the folder
635 folder.addFile(resFile);
642 * Returns true if the path is under /project/res/
643 * @param path a workspace relative path
644 * @return true if the path is under /project res/
646 private boolean isInResFolder(IPath path) {
647 return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
650 private void notifyListenerOnFolderChange(IProject project, ResourceFolder folder,
652 synchronized (mListeners) {
653 for (IResourceListener listener : mListeners) {
655 listener.folderChanged(project, folder, eventType);
656 } catch (Throwable t) {
658 "Failed to execute ResourceManager.IResouceListener.folderChanged()"); //$NON-NLS-1$
664 private void notifyListenerOnFileChange(IProject project, ResourceFile file, int eventType) {
665 synchronized (mListeners) {
666 for (IResourceListener listener : mListeners) {
668 listener.fileChanged(project, file, eventType);
669 } catch (Throwable t) {
671 "Failed to execute ResourceManager.IResouceListener.fileChanged()"); //$NON-NLS-1$
678 * Private constructor to enforce singleton design.
680 private ResourceManager() {
681 // get the default qualifiers.
682 FolderConfiguration defaultConfig = new FolderConfiguration();
683 defaultConfig.createDefault();
684 mQualifiers = defaultConfig.getQualifiers();