Home | History | Annotate | Download | only in gre
      1 /*
      2  * Copyright (C) 2009 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.editors.layout.gre;
     18 
     19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_WIDGET_PREFIX;
     20 import static com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors.VIEW_MERGE;
     21 
     22 import com.android.ide.common.api.DropFeedback;
     23 import com.android.ide.common.api.IDragElement;
     24 import com.android.ide.common.api.IGraphics;
     25 import com.android.ide.common.api.INode;
     26 import com.android.ide.common.api.IViewRule;
     27 import com.android.ide.common.api.InsertType;
     28 import com.android.ide.common.api.Point;
     29 import com.android.ide.common.api.Rect;
     30 import com.android.ide.common.api.RuleAction;
     31 import com.android.ide.common.api.SegmentType;
     32 import com.android.ide.common.layout.ViewRule;
     33 import com.android.ide.eclipse.adt.AdtPlugin;
     34 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     35 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     36 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
     37 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GCWrapper;
     38 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart;
     39 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SimpleElement;
     40 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
     41 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     42 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     43 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     44 import com.android.sdklib.IAndroidTarget;
     45 import com.android.sdklib.internal.project.ProjectProperties;
     46 
     47 import org.eclipse.core.resources.IProject;
     48 import org.eclipse.core.runtime.IStatus;
     49 
     50 import java.io.File;
     51 import java.net.MalformedURLException;
     52 import java.net.URL;
     53 import java.net.URLClassLoader;
     54 import java.util.ArrayList;
     55 import java.util.Collections;
     56 import java.util.HashMap;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Map;
     60 
     61 /**
     62  * The rule engine manages the layout rules and interacts with them.
     63  * There's one {@link RulesEngine} instance per layout editor.
     64  * Each instance has 2 sets of rules: the static ADT rules (shared across all instances)
     65  * and the project specific rules (local to the current instance / layout editor).
     66  */
     67 public class RulesEngine {
     68     private final IProject mProject;
     69     private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>();
     70     /**
     71      * The type of any upcoming node manipulations performed by the {@link IViewRule}s.
     72      * When actions are performed in the tool (like a paste action, or a drag from palette,
     73      * or a drag move within the canvas, etc), these are different types of inserts,
     74      * and we don't want to have the rules track them closely (and pass them back to us
     75      * in the {@link INode#insertChildAt} methods etc), so instead we track the state
     76      * here on behalf of the currently executing rule.
     77      */
     78     private InsertType mInsertType = InsertType.CREATE;
     79 
     80     /**
     81      * Class loader (or null) used to load user/project-specific IViewRule
     82      * classes
     83      */
     84     private ClassLoader mUserClassLoader;
     85 
     86     /**
     87      * Flag set when we've attempted to initialize the {@link #mUserClassLoader}
     88      * already
     89      */
     90     private boolean mUserClassLoaderInited;
     91 
     92     /**
     93      * The editor which owns this {@link RulesEngine}
     94      */
     95     private final GraphicalEditorPart mEditor;
     96 
     97     /**
     98      * Creates a new {@link RulesEngine} associated with the selected project.
     99      * <p/>
    100      * The rules engine will look in the project for a tools jar to load custom view rules.
    101      *
    102      * @param editor the editor which owns this {@link RulesEngine}
    103      * @param project A non-null open project.
    104      */
    105     public RulesEngine(GraphicalEditorPart editor, IProject project) {
    106         mProject = project;
    107         mEditor = editor;
    108     }
    109 
    110     /**
    111      * Find out whether the given project has 3rd party ViewRules, and if so
    112      * return a ClassLoader which can locate them. If not, return null.
    113      * @param project The project to load user rules from
    114      * @return A class loader which can user view rules, or otherwise null
    115      */
    116     private static ClassLoader computeUserClassLoader(IProject project) {
    117         // Default place to locate layout rules. The user may also add to this
    118         // path by defining a config property specifying
    119         // additional .jar files to search via a the layoutrules.jars property.
    120         ProjectState state = Sdk.getProjectState(project);
    121         ProjectProperties projectProperties = state.getProperties();
    122 
    123         // Ensure we have the latest & greatest version of the properties.
    124         // This allows users to reopen editors in a running Eclipse instance
    125         // to get updated view rule jars
    126         projectProperties.reload();
    127 
    128         String path = projectProperties.getProperty(
    129                 ProjectProperties.PROPERTY_RULES_PATH);
    130 
    131         if (path != null && path.length() > 0) {
    132             List<URL> urls = new ArrayList<URL>();
    133             String[] pathElements = path.split(File.pathSeparator);
    134             for (String pathElement : pathElements) {
    135                 pathElement = pathElement.trim(); // Avoid problems with trailing whitespace etc
    136                 File pathFile = new File(pathElement);
    137                 if (!pathFile.isAbsolute()) {
    138                     pathFile = new File(project.getLocation().toFile(), pathElement);
    139                 }
    140                 // Directories and jar files are okay. Do we need to
    141                 // validate the files here as .jar files?
    142                 if (pathFile.isFile() || pathFile.isDirectory()) {
    143                     URL url;
    144                     try {
    145                         url = pathFile.toURI().toURL();
    146                         urls.add(url);
    147                     } catch (MalformedURLException e) {
    148                         AdtPlugin.log(IStatus.WARNING,
    149                                 "Invalid URL: %1$s", //$NON-NLS-1$
    150                                 e.toString());
    151                     }
    152                 }
    153             }
    154 
    155             if (urls.size() > 0) {
    156                 return new URLClassLoader(urls.toArray(new URL[urls.size()]),
    157                         RulesEngine.class.getClassLoader());
    158             }
    159         }
    160 
    161         return null;
    162     }
    163 
    164     /**
    165      * Returns the {@link IProject} on which the {@link RulesEngine} was created.
    166      */
    167     public IProject getProject() {
    168         return mProject;
    169     }
    170 
    171     /**
    172      * Returns the {@link GraphicalEditorPart} for which the {@link RulesEngine} was
    173      * created.
    174      *
    175      * @return the associated editor
    176      */
    177     public GraphicalEditorPart getEditor() {
    178         return mEditor;
    179     }
    180 
    181     /**
    182      * Called by the owner of the {@link RulesEngine} when it is going to be disposed.
    183      * This frees some resources, such as the project's folder monitor.
    184      */
    185     public void dispose() {
    186         clearCache();
    187     }
    188 
    189     /**
    190      * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element.
    191      *
    192      * @param element The view element to target. Can be null.
    193      * @return Null if the rule failed, there's no rule or the rule does not want to override
    194      *   the display name. Otherwise, a string as returned by the rule.
    195      */
    196     public String callGetDisplayName(UiViewElementNode element) {
    197         // try to find a rule for this element's FQCN
    198         IViewRule rule = loadRule(element);
    199 
    200         if (rule != null) {
    201             try {
    202                 return rule.getDisplayName();
    203 
    204             } catch (Exception e) {
    205                 AdtPlugin.log(e, "%s.getDisplayName() failed: %s",
    206                         rule.getClass().getSimpleName(),
    207                         e.toString());
    208             }
    209         }
    210 
    211         return null;
    212     }
    213 
    214     /**
    215      * Invokes {@link IViewRule#addContextMenuActions(List, INode)} on the rule matching the specified element.
    216      *
    217      * @param selectedNode The node selected. Never null.
    218      * @return Null if the rule failed, there's no rule or the rule does not provide
    219      *   any custom menu actions. Otherwise, a list of {@link RuleAction}.
    220      */
    221     public List<RuleAction> callGetContextMenu(NodeProxy selectedNode) {
    222         // try to find a rule for this element's FQCN
    223         IViewRule rule = loadRule(selectedNode.getNode());
    224 
    225         if (rule != null) {
    226             try {
    227                 mInsertType = InsertType.CREATE;
    228                 List<RuleAction> actions = new ArrayList<RuleAction>();
    229                 rule.addContextMenuActions(actions, selectedNode);
    230                 Collections.sort(actions);
    231 
    232                 return actions;
    233             } catch (Exception e) {
    234                 AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
    235                         rule.getClass().getSimpleName(),
    236                         e.toString());
    237             }
    238         }
    239 
    240         return null;
    241     }
    242 
    243     /**
    244      * Invokes {@link IViewRule#addLayoutActions(List, INode, List)} on the rule
    245      * matching the specified element.
    246      *
    247      * @param actions The list of actions to add layout actions into
    248      * @param parentNode The layout node
    249      * @param children The selected children of the node, if any (used to
    250      *            initialize values of child layout controls, if applicable)
    251      * @return Null if the rule failed, there's no rule or the rule does not
    252      *         provide any custom menu actions. Otherwise, a list of
    253      *         {@link RuleAction}.
    254      */
    255     public List<RuleAction> callAddLayoutActions(List<RuleAction> actions,
    256             NodeProxy parentNode, List<NodeProxy> children ) {
    257         // try to find a rule for this element's FQCN
    258         IViewRule rule = loadRule(parentNode.getNode());
    259 
    260         if (rule != null) {
    261             try {
    262                 mInsertType = InsertType.CREATE;
    263                 rule.addLayoutActions(actions, parentNode, children);
    264             } catch (Exception e) {
    265                 AdtPlugin.log(e, "%s.getContextMenu() failed: %s",
    266                         rule.getClass().getSimpleName(),
    267                         e.toString());
    268             }
    269         }
    270 
    271         return null;
    272     }
    273 
    274     /**
    275      * Invokes {@link IViewRule#getSelectionHint(INode, INode)}
    276      * on the rule matching the specified element.
    277      *
    278      * @param parentNode The parent of the node selected. Never null.
    279      * @param childNode The child node that was selected. Never null.
    280      * @return a list of strings to be displayed, or null or empty to display nothing
    281      */
    282     public List<String> callGetSelectionHint(NodeProxy parentNode, NodeProxy childNode) {
    283         // try to find a rule for this element's FQCN
    284         IViewRule rule = loadRule(parentNode.getNode());
    285 
    286         if (rule != null) {
    287             try {
    288                 return rule.getSelectionHint(parentNode, childNode);
    289 
    290             } catch (Exception e) {
    291                 AdtPlugin.log(e, "%s.getSelectionHint() failed: %s",
    292                         rule.getClass().getSimpleName(),
    293                         e.toString());
    294             }
    295         }
    296 
    297         return null;
    298     }
    299 
    300     public void callPaintSelectionFeedback(GCWrapper gcWrapper, NodeProxy parentNode,
    301             List<? extends INode> childNodes, Object view) {
    302         // try to find a rule for this element's FQCN
    303         IViewRule rule = loadRule(parentNode.getNode());
    304 
    305         if (rule != null) {
    306             try {
    307                 rule.paintSelectionFeedback(gcWrapper, parentNode, childNodes, view);
    308 
    309             } catch (Exception e) {
    310                 AdtPlugin.log(e, "%s.callPaintSelectionFeedback() failed: %s",
    311                         rule.getClass().getSimpleName(),
    312                         e.toString());
    313             }
    314         }
    315     }
    316 
    317     /**
    318      * Called when the d'n'd starts dragging over the target node.
    319      * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint.
    320      * If not interested in drop, return false.
    321      * Followed by a paint.
    322      */
    323     public DropFeedback callOnDropEnter(NodeProxy targetNode,
    324             Object targetView, IDragElement[] elements) {
    325         // try to find a rule for this element's FQCN
    326         IViewRule rule = loadRule(targetNode.getNode());
    327 
    328         if (rule != null) {
    329             try {
    330                 return rule.onDropEnter(targetNode, targetView, elements);
    331 
    332             } catch (Exception e) {
    333                 AdtPlugin.log(e, "%s.onDropEnter() failed: %s",
    334                         rule.getClass().getSimpleName(),
    335                         e.toString());
    336             }
    337         }
    338 
    339         return null;
    340     }
    341 
    342     /**
    343      * Called after onDropEnter.
    344      * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same
    345      * as input one).
    346      */
    347     public DropFeedback callOnDropMove(NodeProxy targetNode,
    348             IDragElement[] elements,
    349             DropFeedback feedback,
    350             Point where) {
    351         // try to find a rule for this element's FQCN
    352         IViewRule rule = loadRule(targetNode.getNode());
    353 
    354         if (rule != null) {
    355             try {
    356                 return rule.onDropMove(targetNode, elements, feedback, where);
    357 
    358             } catch (Exception e) {
    359                 AdtPlugin.log(e, "%s.onDropMove() failed: %s",
    360                         rule.getClass().getSimpleName(),
    361                         e.toString());
    362             }
    363         }
    364 
    365         return null;
    366     }
    367 
    368     /**
    369      * Called when drop leaves the target without actually dropping
    370      */
    371     public void callOnDropLeave(NodeProxy targetNode,
    372             IDragElement[] elements,
    373             DropFeedback feedback) {
    374         // try to find a rule for this element's FQCN
    375         IViewRule rule = loadRule(targetNode.getNode());
    376 
    377         if (rule != null) {
    378             try {
    379                 rule.onDropLeave(targetNode, elements, feedback);
    380 
    381             } catch (Exception e) {
    382                 AdtPlugin.log(e, "%s.onDropLeave() failed: %s",
    383                         rule.getClass().getSimpleName(),
    384                         e.toString());
    385             }
    386         }
    387     }
    388 
    389     /**
    390      * Called when drop is released over the target to perform the actual drop.
    391      */
    392     public void callOnDropped(NodeProxy targetNode,
    393             IDragElement[] elements,
    394             DropFeedback feedback,
    395             Point where,
    396             InsertType insertType) {
    397         // try to find a rule for this element's FQCN
    398         IViewRule rule = loadRule(targetNode.getNode());
    399 
    400         if (rule != null) {
    401             try {
    402                 mInsertType = insertType;
    403                 rule.onDropped(targetNode, elements, feedback, where);
    404 
    405             } catch (Exception e) {
    406                 AdtPlugin.log(e, "%s.onDropped() failed: %s",
    407                         rule.getClass().getSimpleName(),
    408                         e.toString());
    409             }
    410         }
    411     }
    412 
    413     /**
    414      * Called when a paint has been requested via DropFeedback.
    415      */
    416     public void callDropFeedbackPaint(IGraphics gc,
    417             NodeProxy targetNode,
    418             DropFeedback feedback) {
    419         if (gc != null && feedback != null && feedback.painter != null) {
    420             try {
    421                 feedback.painter.paint(gc, targetNode, feedback);
    422             } catch (Exception e) {
    423                 AdtPlugin.log(e, "DropFeedback.painter failed: %s",
    424                         e.toString());
    425             }
    426         }
    427     }
    428 
    429     /**
    430      * Called when pasting elements in an existing document on the selected target.
    431      *
    432      * @param targetNode The first node selected.
    433      * @param targetView The view object for the target node, or null if not known
    434      * @param pastedElements The elements being pasted.
    435      */
    436     public void callOnPaste(NodeProxy targetNode, Object targetView,
    437             SimpleElement[] pastedElements) {
    438         // try to find a rule for this element's FQCN
    439         IViewRule rule = loadRule(targetNode.getNode());
    440 
    441         if (rule != null) {
    442             try {
    443                 mInsertType = InsertType.PASTE;
    444                 rule.onPaste(targetNode, targetView, pastedElements);
    445 
    446             } catch (Exception e) {
    447                 AdtPlugin.log(e, "%s.onPaste() failed: %s",
    448                         rule.getClass().getSimpleName(),
    449                         e.toString());
    450             }
    451         }
    452     }
    453 
    454     // ---- Resize operations ----
    455 
    456     public DropFeedback callOnResizeBegin(NodeProxy child, NodeProxy parent, Rect newBounds,
    457             SegmentType horizontalEdge, SegmentType verticalEdge, Object childView,
    458             Object parentView) {
    459         IViewRule rule = loadRule(parent.getNode());
    460 
    461         if (rule != null) {
    462             try {
    463                 return rule.onResizeBegin(child, parent, horizontalEdge, verticalEdge,
    464                         childView, parentView);
    465             } catch (Exception e) {
    466                 AdtPlugin.log(e, "%s.onResizeBegin() failed: %s", rule.getClass().getSimpleName(),
    467                         e.toString());
    468             }
    469         }
    470 
    471         return null;
    472     }
    473 
    474     public void callOnResizeUpdate(DropFeedback feedback, NodeProxy child, NodeProxy parent,
    475             Rect newBounds, int modifierMask) {
    476         IViewRule rule = loadRule(parent.getNode());
    477 
    478         if (rule != null) {
    479             try {
    480                 rule.onResizeUpdate(feedback, child, parent, newBounds, modifierMask);
    481             } catch (Exception e) {
    482                 AdtPlugin.log(e, "%s.onResizeUpdate() failed: %s", rule.getClass().getSimpleName(),
    483                         e.toString());
    484             }
    485         }
    486     }
    487 
    488     public void callOnResizeEnd(DropFeedback feedback, NodeProxy child, NodeProxy parent,
    489             Rect newBounds) {
    490         IViewRule rule = loadRule(parent.getNode());
    491 
    492         if (rule != null) {
    493             try {
    494                 rule.onResizeEnd(feedback, child, parent, newBounds);
    495             } catch (Exception e) {
    496                 AdtPlugin.log(e, "%s.onResizeEnd() failed: %s", rule.getClass().getSimpleName(),
    497                         e.toString());
    498             }
    499         }
    500     }
    501 
    502     // ---- Creation customizations ----
    503 
    504     /**
    505      * Invokes the create hooks ({@link IViewRule#onCreate},
    506      * {@link IViewRule#onChildInserted} when a new child has been created/pasted/moved, and
    507      * is inserted into a given parent. The parent may be null (for example when rendering
    508      * top level items for preview).
    509      *
    510      * @param editor the XML editor to apply edits to the model for (performed by view
    511      *            rules)
    512      * @param parentNode the parent XML node, or null if unknown
    513      * @param childNode the XML node of the new node, never null
    514      * @param overrideInsertType If not null, specifies an explicit insert type to use for
    515      *            edits made during the customization
    516      */
    517     public void callCreateHooks(
    518         AndroidXmlEditor editor,
    519         NodeProxy parentNode, NodeProxy childNode,
    520         InsertType overrideInsertType) {
    521         IViewRule parentRule = null;
    522 
    523         if (parentNode != null) {
    524             UiViewElementNode parentUiNode = parentNode.getNode();
    525             parentRule = loadRule(parentUiNode);
    526         }
    527 
    528         if (overrideInsertType != null) {
    529             mInsertType = overrideInsertType;
    530         }
    531 
    532         UiViewElementNode newUiNode = childNode.getNode();
    533         IViewRule childRule = loadRule(newUiNode);
    534         if (childRule != null || parentRule != null) {
    535             callCreateHooks(editor, mInsertType, parentRule, parentNode,
    536                     childRule, childNode);
    537         }
    538     }
    539 
    540     private static void callCreateHooks(
    541             final AndroidXmlEditor editor, final InsertType insertType,
    542             final IViewRule parentRule, final INode parentNode,
    543             final IViewRule childRule, final INode newNode) {
    544         // Notify the parent about the new child in case it wants to customize it
    545         // (For example, a ScrollView parent can go and set all its children's layout params to
    546         // fill the parent.)
    547         if (!editor.isEditXmlModelPending()) {
    548             editor.wrapEditXmlModel(new Runnable() {
    549                 public void run() {
    550                     callCreateHooks(editor, insertType,
    551                             parentRule, parentNode, childRule, newNode);
    552                 }
    553             });
    554             return;
    555         }
    556 
    557         if (parentRule != null) {
    558             parentRule.onChildInserted(newNode, parentNode, insertType);
    559         }
    560 
    561         // Look up corresponding IViewRule, and notify the rule about
    562         // this create action in case it wants to customize the new object.
    563         // (For example, a rule for TabHosts can go and create a default child tab
    564         // when you create it.)
    565         if (childRule != null) {
    566             childRule.onCreate(newNode, parentNode, insertType);
    567         }
    568 
    569         if (parentNode != null) {
    570             ((NodeProxy) parentNode).applyPendingChanges();
    571         }
    572     }
    573 
    574     /**
    575      * Set the type of insert currently in progress
    576      *
    577      * @param insertType the insert type to use for the next operation
    578      */
    579     public void setInsertType(InsertType insertType) {
    580         mInsertType = insertType;
    581     }
    582 
    583     /**
    584      * Return the type of insert currently in progress
    585      *
    586      * @return the type of insert currently in progress
    587      */
    588     public InsertType getInsertType() {
    589         return mInsertType;
    590     }
    591 
    592     // ---- Deletion ----
    593 
    594     public void callOnRemovingChildren(AndroidXmlEditor editor, NodeProxy parentNode,
    595             List<INode> children) {
    596         if (parentNode != null) {
    597             UiViewElementNode parentUiNode = parentNode.getNode();
    598             IViewRule parentRule = loadRule(parentUiNode);
    599             if (parentRule != null) {
    600                 parentRule.onRemovingChildren(children, parentNode);
    601             }
    602         }
    603     }
    604 
    605     // ---- private ---
    606 
    607     /**
    608      * Returns the descriptor for the base View class.
    609      * This could be null if the SDK or the given platform target hasn't loaded yet.
    610      */
    611     private ViewElementDescriptor getBaseViewDescriptor() {
    612         Sdk currentSdk = Sdk.getCurrent();
    613         if (currentSdk != null) {
    614             IAndroidTarget target = currentSdk.getTarget(mProject);
    615             if (target != null) {
    616                 AndroidTargetData data = currentSdk.getTargetData(target);
    617                 return data.getLayoutDescriptors().getBaseViewDescriptor();
    618             }
    619         }
    620         return null;
    621     }
    622 
    623     /**
    624      * Clear the Rules cache. Calls onDispose() on each rule.
    625      */
    626     private void clearCache() {
    627         // The cache can contain multiple times the same rule instance for different
    628         // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer
    629         // all values to a unique set.
    630         HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values());
    631 
    632         mRulesCache.clear();
    633 
    634         for (IViewRule rule : rules) {
    635             if (rule != null) {
    636                 try {
    637                     rule.onDispose();
    638                 } catch (Exception e) {
    639                     AdtPlugin.log(e, "%s.onDispose() failed: %s",
    640                             rule.getClass().getSimpleName(),
    641                             e.toString());
    642                 }
    643             }
    644         }
    645     }
    646 
    647     /**
    648      * Load a rule using its descriptor. This will try to first load the rule using its
    649      * actual FQCN and if that fails will find the first parent that works in the view
    650      * hierarchy.
    651      */
    652     private IViewRule loadRule(UiViewElementNode element) {
    653         if (element == null) {
    654             return null;
    655         }
    656 
    657         String targetFqcn = null;
    658         ViewElementDescriptor targetDesc = null;
    659 
    660         ElementDescriptor d = element.getDescriptor();
    661         if (d instanceof ViewElementDescriptor) {
    662             targetDesc = (ViewElementDescriptor) d;
    663         }
    664         if (d == null || !(d instanceof ViewElementDescriptor)) {
    665             // This should not happen. All views should have some kind of *view* element
    666             // descriptor. Maybe the project is not complete and doesn't build or something.
    667             // In this case, we'll use the descriptor of the base android View class.
    668             targetDesc = getBaseViewDescriptor();
    669         }
    670 
    671 
    672         // Return the rule if we find it in the cache, even if it was stored as null
    673         // (which means we didn't find it earlier, so don't look for it again)
    674         IViewRule rule = mRulesCache.get(targetDesc);
    675         if (rule != null || mRulesCache.containsKey(targetDesc)) {
    676             return rule;
    677         }
    678 
    679         // Get the descriptor and loop through the super class hierarchy
    680         for (ViewElementDescriptor desc = targetDesc;
    681                 desc != null;
    682                 desc = desc.getSuperClassDesc()) {
    683 
    684             // Get the FQCN of this View
    685             String fqcn = desc.getFullClassName();
    686             if (fqcn == null) {
    687                 // Shouldn't be happening.
    688                 return null;
    689             }
    690 
    691             // The first time we keep the FQCN around as it's the target class we were
    692             // initially trying to load. After, as we move through the hierarchy, the
    693             // target FQCN remains constant.
    694             if (targetFqcn == null) {
    695                 targetFqcn = fqcn;
    696             }
    697 
    698             if (fqcn.indexOf('.') == -1) {
    699                 // Deal with unknown descriptors; these lack the full qualified path and
    700                 // elements in the layout without a package are taken to be in the
    701                 // android.widget package.
    702                 fqcn = ANDROID_WIDGET_PREFIX + fqcn;
    703             }
    704 
    705             // Try to find a rule matching the "real" FQCN. If we find it, we're done.
    706             // If not, the for loop will move to the parent descriptor.
    707             rule = loadRule(fqcn, targetFqcn);
    708             if (rule != null) {
    709                 // We found one.
    710                 // As a side effect, loadRule() also cached the rule using the target FQCN.
    711                 return rule;
    712             }
    713         }
    714 
    715         // Memorize in the cache that we couldn't find a rule for this descriptor
    716         mRulesCache.put(targetDesc, null);
    717         return null;
    718     }
    719 
    720     /**
    721      * Try to load a rule given a specific FQCN. This looks for an exact match in either
    722      * the ADT scripts or the project scripts and does not look at parent hierarchy.
    723      * <p/>
    724      * Once a rule is found (or not), it is stored in a cache using its target FQCN
    725      * so we don't try to reload it.
    726      * <p/>
    727      * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
    728      * where target FQCN is the class we were initially looking for, which might be the same as
    729      * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
    730      *
    731      * @param realFqcn The FQCN of the rule class actually being loaded.
    732      * @param targetFqcn The FQCN of the class actually processed, which might be different from
    733      *          the FQCN of the rule being loaded.
    734      */
    735     IViewRule loadRule(String realFqcn, String targetFqcn) {
    736         if (realFqcn == null || targetFqcn == null) {
    737             return null;
    738         }
    739 
    740         // Return the rule if we find it in the cache, even if it was stored as null
    741         // (which means we didn't find it earlier, so don't look for it again)
    742         IViewRule rule = mRulesCache.get(realFqcn);
    743         if (rule != null || mRulesCache.containsKey(realFqcn)) {
    744             return rule;
    745         }
    746 
    747         // Look for class via reflection
    748         try {
    749             // For now, we package view rules for the builtin Android views and
    750             // widgets with the tool in a special package, so look there rather
    751             // than in the same package as the widgets.
    752             String ruleClassName;
    753             ClassLoader classLoader;
    754             if (realFqcn.startsWith("android.") || //$NON-NLS-1$
    755                     realFqcn.equals(VIEW_MERGE) ||
    756                     realFqcn.endsWith(".GridLayout") || //$NON-NLS-1$ // Temporary special case
    757                     // FIXME: Remove this special case as soon as we pull
    758                     // the MapViewRule out of this code base and bundle it
    759                     // with the add ons
    760                     realFqcn.startsWith("com.google.android.maps.")) { //$NON-NLS-1$
    761                 // This doesn't handle a case where there are name conflicts
    762                 // (e.g. where there are multiple different views with the same
    763                 // class name and only differing in package names, but that's a
    764                 // really bad practice in the first place, and if that situation
    765                 // should come up in the API we can enhance this algorithm.
    766                 String packageName = ViewRule.class.getName();
    767                 packageName = packageName.substring(0, packageName.lastIndexOf('.'));
    768                 classLoader = RulesEngine.class.getClassLoader();
    769                 int dotIndex = realFqcn.lastIndexOf('.');
    770                 String baseName = realFqcn.substring(dotIndex+1);
    771                 // Capitalize rule class name to match naming conventions, if necessary (<merge>)
    772                 if (Character.isLowerCase(baseName.charAt(0))) {
    773                     baseName = Character.toUpperCase(baseName.charAt(0)) + baseName.substring(1);
    774                 }
    775                 ruleClassName = packageName + "." + //$NON-NLS-1$
    776                     baseName + "Rule"; //$NON-NLS-1$
    777             } else {
    778                 // Initialize the user-classpath for 3rd party IViewRules, if necessary
    779                 if (mUserClassLoader == null) {
    780                     // Only attempt to load rule paths once (per RulesEngine instance);
    781                     if (!mUserClassLoaderInited) {
    782                         mUserClassLoaderInited = true;
    783                         mUserClassLoader = computeUserClassLoader(mProject);
    784                     }
    785 
    786                     if (mUserClassLoader == null) {
    787                         // The mUserClassLoader can be null; this is the typical scenario,
    788                         // when the user is only using builtin layout rules.
    789                         // This means however we can't resolve this fqcn since it's not
    790                         // in the name space of the builtin rules.
    791                         mRulesCache.put(realFqcn, null);
    792                         return null;
    793                     }
    794                 }
    795 
    796                 // For other (3rd party) widgets, look in the same package (though most
    797                 // likely not in the same jar!)
    798                 ruleClassName = realFqcn + "Rule"; //$NON-NLS-1$
    799                 classLoader = mUserClassLoader;
    800             }
    801 
    802             Class<?> clz = Class.forName(ruleClassName, true, classLoader);
    803             rule = (IViewRule) clz.newInstance();
    804             return initializeRule(rule, targetFqcn);
    805         } catch (ClassNotFoundException ex) {
    806             // Not an unexpected error - this means that there isn't a helper for this
    807             // class.
    808         } catch (InstantiationException e) {
    809             // This is NOT an expected error: fail.
    810             AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
    811         } catch (IllegalAccessException e) {
    812             // This is NOT an expected error: fail.
    813             AdtPlugin.log(e, "load rule error (%s): %s", realFqcn, e.toString());
    814         }
    815 
    816         // Memorize in the cache that we couldn't find a rule for this real FQCN
    817         mRulesCache.put(realFqcn, null);
    818         return null;
    819     }
    820 
    821     /**
    822      * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN
    823      * and bail out.
    824      * <p/>
    825      * Contract: the rule is not in the {@link #mRulesCache} yet and this method will
    826      * cache it using the target FQCN if the rule is accepted.
    827      * <p/>
    828      * The real FQCN is the actual rule class we're loading, e.g. "android.view.View"
    829      * where target FQCN is the class we were initially looking for, which might be the same as
    830      * the real FQCN or might be a derived class, e.g. "android.widget.TextView".
    831      *
    832      * @param rule A rule freshly loaded.
    833      * @param targetFqcn The FQCN of the class actually processed, which might be different from
    834      *          the FQCN of the rule being loaded.
    835      * @return The rule if accepted, or null if the rule can't handle that FQCN.
    836      */
    837     private IViewRule initializeRule(IViewRule rule, String targetFqcn) {
    838 
    839         try {
    840             if (rule.onInitialize(targetFqcn, new ClientRulesEngine(this, targetFqcn))) {
    841                 // Add it to the cache and return it
    842                 mRulesCache.put(targetFqcn, rule);
    843                 return rule;
    844             } else {
    845                 rule.onDispose();
    846             }
    847         } catch (Exception e) {
    848             AdtPlugin.log(e, "%s.onInit() failed: %s",
    849                     rule.getClass().getSimpleName(),
    850                     e.toString());
    851         }
    852 
    853         return null;
    854     }
    855 }
    856