1 /*
2  * Copyright (C) 2007 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.eclipse.adt.internal.resources.manager;
18 
19 import com.android.SdkConstants;
20 import com.android.annotations.NonNull;
21 import com.android.annotations.Nullable;
22 import com.android.ide.common.resources.IntArrayWrapper;
23 import com.android.ide.common.xml.ManifestData;
24 import com.android.ide.eclipse.adt.AdtConstants;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
27 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
29 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IProjectListener;
30 import com.android.resources.ResourceType;
31 import com.android.util.Pair;
32 
33 import org.eclipse.core.resources.IFile;
34 import org.eclipse.core.resources.IMarkerDelta;
35 import org.eclipse.core.resources.IProject;
36 import org.eclipse.core.resources.IResource;
37 import org.eclipse.core.resources.IResourceDelta;
38 import org.eclipse.core.runtime.CoreException;
39 import org.eclipse.core.runtime.IPath;
40 import org.eclipse.core.runtime.IStatus;
41 
42 import java.io.File;
43 import java.lang.reflect.Field;
44 import java.lang.reflect.Modifier;
45 import java.util.EnumMap;
46 import java.util.HashMap;
47 import java.util.Map;
48 import java.util.regex.Pattern;
49 
50 /**
51  * A monitor for the compiled resources. This only monitors changes in the resources of type
52  *  {@link ResourceType#ID}.
53  */
54 public final class CompiledResourcesMonitor implements IFileListener, IProjectListener {
55 
56     private final static CompiledResourcesMonitor sThis = new CompiledResourcesMonitor();
57 
58     /**
59      * Sets up the monitoring system.
60      * @param monitor The main Resource Monitor.
61      */
setupMonitor(GlobalProjectMonitor monitor)62     public static void setupMonitor(GlobalProjectMonitor monitor) {
63         monitor.addFileListener(sThis, IResourceDelta.ADDED | IResourceDelta.CHANGED);
64         monitor.addProjectListener(sThis);
65     }
66 
67     /**
68      * private constructor to prevent construction.
69      */
CompiledResourcesMonitor()70     private CompiledResourcesMonitor() {
71     }
72 
73 
74     /* (non-Javadoc)
75      * Sent when a file changed : if the file is the R class, then it is parsed again to update
76      * the internal data.
77      *
78      * @param file The file that changed.
79      * @param markerDeltas The marker deltas for the file.
80      * @param kind The change kind. This is equivalent to
81      * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
82      *
83      * @see IFileListener#fileChanged
84      */
85     @Override
fileChanged(@onNull IFile file, @NonNull IMarkerDelta[] markerDeltas, int kind, @Nullable String extension, int flags, boolean isAndroidProject)86     public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
87             int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
88         if (!isAndroidProject || flags == IResourceDelta.MARKERS) {
89             // Not Android or only the markers changed: not relevant
90             return;
91         }
92 
93         IProject project = file.getProject();
94 
95         if (file.getName().equals(SdkConstants.FN_COMPILED_RESOURCE_CLASS)) {
96             // create the classname
97             String className = getRClassName(project);
98             if (className == null) {
99                 // We need to abort.
100                 AdtPlugin.log(IStatus.ERROR,
101                         "fileChanged: failed to find manifest package for project %1$s", //$NON-NLS-1$
102                         project.getName());
103                 return;
104             }
105             // path will begin with /projectName/bin/classes so we'll ignore that
106             IPath relativeClassPath = file.getFullPath().removeFirstSegments(3);
107             if (packagePathMatches(relativeClassPath.toString(), className)) {
108                 loadAndParseRClass(project, className);
109             }
110         }
111     }
112 
113     /**
114      * Check to see if the package section of the given path matches the packageName.
115      * For example, /project/bin/classes/com/foo/app/R.class should match com.foo.app.R
116      * @param path the pathname of the file to look at
117      * @param packageName the package qualified name of the class
118      * @return true if the package section of the path matches the package qualified name
119      */
packagePathMatches(String path, String packageName)120     private boolean packagePathMatches(String path, String packageName) {
121         // First strip the ".class" off the end of the path
122         String pathWithoutExtension = path.substring(0, path.indexOf(SdkConstants.DOT_CLASS));
123 
124         // then split the components of each path by their separators
125         String [] pathArray = pathWithoutExtension.split(Pattern.quote(File.separator));
126         String [] packageArray = packageName.split(AdtConstants.RE_DOT);
127 
128 
129         int pathIndex = 0;
130         int packageIndex = 0;
131 
132         while (pathIndex < pathArray.length && packageIndex < packageArray.length) {
133             if (pathArray[pathIndex].equals(packageArray[packageIndex]) == false) {
134                 return false;
135             }
136             pathIndex++;
137             packageIndex++;
138         }
139         // We may have matched all the way up to this point, but we're not sure it's a match
140         // unless BOTH paths done
141         return (pathIndex == pathArray.length && packageIndex == packageArray.length);
142     }
143 
144     /**
145      * Processes project close event.
146      */
147     @Override
projectClosed(IProject project)148     public void projectClosed(IProject project) {
149         // the ProjectResources object will be removed by the ResourceManager.
150     }
151 
152     /**
153      * Processes project delete event.
154      */
155     @Override
projectDeleted(IProject project)156     public void projectDeleted(IProject project) {
157         // the ProjectResources object will be removed by the ResourceManager.
158     }
159 
160     /**
161      * Processes project open event.
162      */
163     @Override
projectOpened(IProject project)164     public void projectOpened(IProject project) {
165         // when the project is opened, we get an ADDED event for each file, so we don't
166         // need to do anything here.
167     }
168 
169     @Override
projectRenamed(IProject project, IPath from)170     public void projectRenamed(IProject project, IPath from) {
171         // renamed projects also trigger delete/open event,
172         // so nothing to be done here.
173     }
174 
175     /**
176      * Processes existing project at init time.
177      */
178     @Override
projectOpenedWithWorkspace(IProject project)179     public void projectOpenedWithWorkspace(IProject project) {
180         try {
181             // check this is an android project
182             if (project.hasNature(AdtConstants.NATURE_DEFAULT)) {
183                 String className = getRClassName(project);
184                 // Find the classname
185                 if (className == null) {
186                     // We need to abort.
187                     AdtPlugin.log(IStatus.ERROR,
188                             "projectOpenedWithWorkspace: failed to find manifest package for project %1$s", //$NON-NLS-1$
189                             project.getName());
190                     return;
191                 }
192                 loadAndParseRClass(project, className);
193             }
194         } catch (CoreException e) {
195             // pass
196         }
197     }
198 
199     @Override
allProjectsOpenedWithWorkspace()200     public void allProjectsOpenedWithWorkspace() {
201         // nothing to do.
202     }
203 
204 
loadAndParseRClass(IProject project, String className)205     private void loadAndParseRClass(IProject project, String className) {
206         try {
207             // first check there's a ProjectResources to store the content
208             ProjectResources projectResources = ResourceManager.getInstance().getProjectResources(
209                     project);
210 
211             if (projectResources != null) {
212                 // create a temporary class loader to load the class
213                 ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */,
214                         project);
215 
216                 try {
217                     Class<?> clazz = loader.loadClass(className);
218 
219                     if (clazz != null) {
220                         // create the maps to store the result of the parsing
221                         Map<ResourceType, Map<String, Integer>> resourceValueMap =
222                             new EnumMap<ResourceType, Map<String, Integer>>(ResourceType.class);
223                         Map<Integer, Pair<ResourceType, String>> genericValueToNameMap =
224                             new HashMap<Integer, Pair<ResourceType, String>>();
225                         Map<IntArrayWrapper, String> styleableValueToNameMap =
226                             new HashMap<IntArrayWrapper, String>();
227 
228                         // parse the class
229                         if (parseClass(clazz, genericValueToNameMap, styleableValueToNameMap,
230                                 resourceValueMap)) {
231                             // now we associate the maps to the project.
232                             projectResources.setCompiledResources(genericValueToNameMap,
233                                     styleableValueToNameMap, resourceValueMap);
234                         }
235                     }
236                 } catch (Error e) {
237                     // Log this error with the class name we're trying to load and abort.
238                     AdtPlugin.log(e, "loadAndParseRClass failed to find class %1$s", className); //$NON-NLS-1$
239                 }
240             }
241         } catch (ClassNotFoundException e) {
242             // pass
243         }
244     }
245 
246     /**
247      * Parses a R class, and fills maps.
248      * @param rClass the class to parse
249      * @param genericValueToNameMap
250      * @param styleableValueToNameMap
251      * @param resourceValueMap
252      * @return True if we managed to parse the R class.
253      */
parseClass(Class<?> rClass, Map<Integer, Pair<ResourceType, String>> genericValueToNameMap, Map<IntArrayWrapper, String> styleableValueToNameMap, Map<ResourceType, Map<String, Integer>> resourceValueMap)254     private boolean parseClass(Class<?> rClass,
255             Map<Integer, Pair<ResourceType, String>> genericValueToNameMap,
256             Map<IntArrayWrapper, String> styleableValueToNameMap, Map<ResourceType,
257             Map<String, Integer>> resourceValueMap) {
258         try {
259             for (Class<?> inner : rClass.getDeclaredClasses()) {
260                 String resTypeName = inner.getSimpleName();
261                 ResourceType resType = ResourceType.getEnum(resTypeName);
262 
263                 if (resType != null) {
264                     Map<String, Integer> fullMap = new HashMap<String, Integer>();
265                     resourceValueMap.put(resType, fullMap);
266 
267                     for (Field f : inner.getDeclaredFields()) {
268                         // only process static final fields.
269                         int modifiers = f.getModifiers();
270                         if (Modifier.isStatic(modifiers)) {
271                             Class<?> type = f.getType();
272                             if (type.isArray() && type.getComponentType() == int.class) {
273                                 // if the object is an int[] we put it in the styleable map
274                                 styleableValueToNameMap.put(
275                                         new IntArrayWrapper((int[]) f.get(null)),
276                                         f.getName());
277                             } else if (type == int.class) {
278                                 Integer value = (Integer) f.get(null);
279                                 genericValueToNameMap.put(value, Pair.of(resType, f.getName()));
280                                 fullMap.put(f.getName(), value);
281                             } else {
282                                 assert false;
283                             }
284                         }
285                     }
286                 }
287             }
288 
289             return true;
290         } catch (IllegalArgumentException e) {
291         } catch (IllegalAccessException e) {
292         }
293         return false;
294     }
295 
296     /**
297      * Returns the class name of the R class, based on the project's manifest's package.
298      *
299      * @return A class name (e.g. "my.app.R") or null if there's no valid package in the manifest.
300      */
getRClassName(IProject project)301     private String getRClassName(IProject project) {
302         IFile manifestFile = ProjectHelper.getManifest(project);
303         if (manifestFile != null && manifestFile.isSynchronized(IResource.DEPTH_ZERO)) {
304             ManifestData data = AndroidManifestHelper.parseForData(manifestFile);
305             if (data != null) {
306                 String javaPackage = data.getPackage();
307                 return javaPackage + ".R"; //$NON-NLS-1$
308             }
309         }
310         return null;
311     }
312 
313 }
314