| /* |
| * Copyright (C) 2009 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.eclipse.adt.internal.editors.layout.gre; |
| |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; |
| import com.android.ide.eclipse.adt.internal.resources.manager.ResourceMonitor; |
| import com.android.ide.eclipse.adt.internal.resources.manager.ResourceMonitor.IFolderListener; |
| |
| import org.codehaus.groovy.control.CompilationFailedException; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceDelta; |
| |
| import groovy.lang.GroovyClassLoader; |
| |
| import java.io.InputStream; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| |
| /** |
| * The rule engine manages the groovy rules files and interacts with them. |
| * There's one {@link RulesEngine} instance per layout editor. |
| * Each instance has 2 sets of scripts: the static ADT rules (shared across all instances) |
| * and the project specific rules (local to the current instance / layout editor). |
| */ |
| public class RulesEngine { |
| |
| private static final String PROJECT_SCRIPT_DIR = "gscripts"; |
| private static final String SCRIPT_EXT = ".groovy"; //$NON-NLS-1$ |
| |
| private final GroovyClassLoader mClassLoader; |
| private final IProject mProject; |
| private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); |
| private ProjectFolderListener mProjectFolderListener; |
| |
| public RulesEngine(IProject project) { |
| mProject = project; |
| ClassLoader cl = getClass().getClassLoader(); |
| mClassLoader = new GroovyClassLoader(cl); |
| |
| mProjectFolderListener = new ProjectFolderListener(); |
| ResourceMonitor.getMonitor().addFolderListener( |
| mProjectFolderListener, |
| IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED); |
| } |
| |
| /** |
| * Called by the owner of the {@link RulesEngine} when it is going to be disposed. |
| * This frees some resources, such as the project's folder monitor. |
| */ |
| public void dispose() { |
| if (mProjectFolderListener != null) { |
| ResourceMonitor.getMonitor().removeFolderListener(mProjectFolderListener); |
| mProjectFolderListener = null; |
| } |
| clearCache(); |
| } |
| |
| public String getDisplayName(UiViewElementNode element) { |
| // try to find a rule for this element's FQCN |
| IViewRule rule = loadRule(element); |
| |
| if (rule != null) { |
| try { |
| return rule.getDisplayName(); |
| |
| } catch (Exception e) { |
| logError("%s.getDisplayName() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| |
| return null; |
| } |
| |
| |
| // ---- private --- |
| |
| private class ProjectFolderListener implements IFolderListener { |
| public void folderChanged(IFolder folder, int kind) { |
| if (folder.getProject() == mProject && |
| PROJECT_SCRIPT_DIR.equals(folder.getName())) { |
| // Clear our whole rules cache, to not have to deal with dependencies. |
| clearCache(); |
| } |
| } |
| } |
| |
| /** |
| * Clear the Rules cache. Calls onDispose() on each rule. |
| */ |
| private void clearCache() { |
| // The cache can contain multiple times the same rule instance for different |
| // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer |
| // all values to a unique set. |
| HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values()); |
| |
| mRulesCache.clear(); |
| |
| for (IViewRule rule : rules) { |
| if (rule != null) { |
| try { |
| rule.onDispose(); |
| } catch (Exception e) { |
| logError("%s.onDispose() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Load a rule using its descriptor. This will try to first load the rule using its |
| * actual FQCN and if that fails will find the first parent that works in the view |
| * hierarchy. |
| */ |
| private IViewRule loadRule(UiViewElementNode element) { |
| if (element == null) { |
| return null; |
| } else { |
| // sanity check. this can't fail. |
| ElementDescriptor d = element.getDescriptor(); |
| if (d == null || !(d instanceof ViewElementDescriptor)) { |
| return null; |
| } |
| } |
| |
| String targetFqcn = null; |
| ViewElementDescriptor targetDesc = (ViewElementDescriptor) element.getDescriptor(); |
| |
| // Return the rule if we find it in the cache, even if it was stored as null |
| // (which means we didn't find it earlier, so don't look for it again) |
| IViewRule rule = mRulesCache.get(targetDesc); |
| if (rule != null || mRulesCache.containsKey(targetDesc)) { |
| return rule; |
| } |
| |
| // Get the descriptor and loop through the super class hierarchy |
| for (ViewElementDescriptor desc = targetDesc; |
| desc != null; |
| desc = desc.getSuperClassDesc()) { |
| |
| // Get the FQCN of this View |
| String fqcn = desc.getFullClassName(); |
| if (fqcn == null) { |
| return null; |
| } |
| |
| // The first time we keep the FQCN around as it's the target class we were |
| // initially trying to load. After, as we move through the hierarchy, the |
| // target FQCN remains constant. |
| if (targetFqcn == null) { |
| targetFqcn = fqcn; |
| } |
| |
| // Try to find a rule matching the "real" FQCN. If we find it, we're done. |
| // If not, the for loop will move to the parent descriptor. |
| rule = loadRule(fqcn, targetFqcn); |
| if (rule != null) { |
| // We found one. |
| // As a side effect, loadRule() also cached the rule using the target FQCN. |
| return rule; |
| } |
| } |
| |
| // Memorize in the cache that we couldn't find a rule for this descriptor |
| mRulesCache.put(targetDesc, null); |
| return null; |
| } |
| |
| /** |
| * Try to load a rule given a specific FQCN. This looks for an exact match in either |
| * the ADT scripts or the project scripts and does not look at parent hierarchy. |
| * <p/> |
| * Once a rule is found (or not), it is stored in a cache using its target FQCN |
| * so we don't try to reload it. |
| * <p/> |
| * The real FQCN is the actual groovy filename we're loading, e.g. "android.view.View.groovy" |
| * where target FQCN is the class we were initially looking for, which might be the same as |
| * the real FQCN or might be a derived class, e.g. "android.widget.TextView". |
| * |
| * @param realFqcn The FQCN of the groovy rule actually being loaded. |
| * @param targetFqcn The FQCN of the class actually processed, which might be different from |
| * the FQCN of the rule being loaded. |
| */ |
| private IViewRule loadRule(String realFqcn, String targetFqcn) { |
| if (realFqcn == null || targetFqcn == null) { |
| return null; |
| } |
| |
| // Return the rule if we find it in the cache, even if it was stored as null |
| // (which means we didn't find it earlier, so don't look for it again) |
| IViewRule rule = mRulesCache.get(realFqcn); |
| if (rule != null || mRulesCache.containsKey(realFqcn)) { |
| return rule; |
| } |
| |
| // Look for the file in ADT first. |
| // That means a project can't redefine any of the rules we define. |
| String filename = realFqcn + SCRIPT_EXT; |
| |
| try { |
| InputStream is = getClass().getResourceAsStream(filename); |
| rule = loadStream(is, realFqcn); |
| if (rule != null) { |
| return initializeRule(rule, targetFqcn); |
| } |
| } catch (Exception e) { |
| logError("load rule error (%s): %s", filename, e.getMessage()); |
| } |
| |
| // Then look for the file in the project |
| IResource r = mProject.findMember(PROJECT_SCRIPT_DIR); |
| if (r != null && r.getType() == IResource.FOLDER) { |
| r = ((IFolder) r).findMember(filename); |
| if (r != null && r.getType() == IResource.FILE) { |
| try { |
| InputStream is = ((IFile) r).getContents(); |
| rule = loadStream(is, realFqcn); |
| if (rule != null) { |
| return initializeRule(rule, targetFqcn); |
| } |
| } catch (Exception e) { |
| logError("load rule error (%s): %s", filename, e.getMessage()); |
| } |
| } |
| } |
| |
| // Memorize in the cache that we couldn't find a rule for this real FQCN |
| mRulesCache.put(realFqcn, null); |
| return null; |
| } |
| |
| /** |
| * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN |
| * and bail out. |
| * <p/> |
| * Contract: the rule is not in the {@link #mRulesCache} yet and this method will |
| * cache it using the target FQCN if the rule is accepted. |
| * <p/> |
| * The real FQCN is the actual groovy filename we're loading, e.g. "android.view.View.groovy" |
| * where target FQCN is the class we were initially looking for, which might be the same as |
| * the real FQCN or might be a derived class, e.g. "android.widget.TextView". |
| * |
| * @param rule A rule freshly loaded. |
| * @param targetFqcn The FQCN of the class actually processed, which might be different from |
| * the FQCN of the rule being loaded. |
| * @return The rule if accepted, or null if the rule can't handle that FQCN. |
| */ |
| private IViewRule initializeRule(IViewRule rule, String targetFqcn) { |
| |
| try { |
| if (rule.onInitialize(targetFqcn)) { |
| // Add it to the cache and return it |
| mRulesCache.put(targetFqcn, rule); |
| return rule; |
| } else { |
| rule.onDispose(); |
| } |
| } catch (Exception e) { |
| logError("%s.onInit() failed: %s", |
| rule.getClass().getSimpleName(), |
| e.toString()); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Actually load a groovy script and instantiate an {@link IViewRule} from it. |
| * On error, outputs (hopefully meaningful) groovy error messages. |
| * |
| * @param is The input stream for the groovy script. Can be null. |
| * @param fqcn The class name, for display purposes only. |
| * @return A new {@link IViewRule} or null if loading failed for any reason. |
| */ |
| private IViewRule loadStream(InputStream is, String fqcn) { |
| try { |
| if (is == null) { |
| // We handle this case for convenience. It typically means that the |
| // input stream couldn't be opened because the file was not found. |
| // Since we expect this to be a common case, we don't log it as an error. |
| return null; |
| } |
| |
| // Create a groovy class from it. Can fail to compile. |
| Class<?> c = mClassLoader.parseClass(is, fqcn); |
| |
| // Get an instance. This might throw ClassCastException. |
| return (IViewRule) c.newInstance(); |
| |
| } catch (CompilationFailedException e) { |
| logError("Compilation error in %s.groovy: %s", fqcn, e.toString()); |
| } catch (ClassCastException e) { |
| logError("Script %s.groovy does not implement IViewRule", fqcn); |
| } catch (Exception e) { |
| logError("Failed to use %s.groovy: %s", fqcn, e.getMessage()); |
| } |
| |
| return null; |
| } |
| |
| private void logError(String format, Object...params) { |
| String s = String.format(format, params); |
| AdtPlugin.printErrorToConsole(mProject, s); |
| } |
| } |