blob: d890653d54482baeede2e2262a4018b8efddd2b8 [file] [log] [blame]
/*
* 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);
}
}