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