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 com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.AndroidConstants; 21 import com.android.ide.eclipse.adt.editors.layout.gscripts.DropFeedback; 22 import com.android.ide.eclipse.adt.editors.layout.gscripts.IDragElement; 23 import com.android.ide.eclipse.adt.editors.layout.gscripts.IGraphics; 24 import com.android.ide.eclipse.adt.editors.layout.gscripts.INode; 25 import com.android.ide.eclipse.adt.editors.layout.gscripts.IViewRule; 26 import com.android.ide.eclipse.adt.editors.layout.gscripts.Point; 27 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; 28 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 29 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 30 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; 31 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFolderListener; 32 import com.android.sdklib.SdkConstants; 33 34 import org.codehaus.groovy.control.CompilationFailedException; 35 import org.codehaus.groovy.control.CompilationUnit; 36 import org.codehaus.groovy.control.CompilerConfiguration; 37 import org.codehaus.groovy.control.Phases; 38 import org.codehaus.groovy.control.SourceUnit; 39 import org.eclipse.core.resources.IFile; 40 import org.eclipse.core.resources.IFolder; 41 import org.eclipse.core.resources.IProject; 42 import org.eclipse.core.resources.IResource; 43 import org.eclipse.core.resources.IResourceDelta; 44 45 import groovy.lang.GroovyClassLoader; 46 import groovy.lang.GroovyCodeSource; 47 import groovy.lang.GroovyResourceLoader; 48 49 import java.io.InputStream; 50 import java.io.InputStreamReader; 51 import java.net.MalformedURLException; 52 import java.net.URI; 53 import java.net.URL; 54 import java.nio.charset.Charset; 55 import java.security.CodeSource; 56 import java.util.HashMap; 57 import java.util.HashSet; 58 import java.util.Map; 59 60 /* TODO: 61 * - create a logger object and pass it around. 62 * 63 */ 64 65 /** 66 * The rule engine manages the groovy rules files and interacts with them. 67 * There's one {@link RulesEngine} instance per layout editor. 68 * Each instance has 2 sets of scripts: the static ADT rules (shared across all instances) 69 * and the project specific rules (local to the current instance / layout editor). 70 */ 71 public class RulesEngine { 72 73 /** 74 * The project folder where the scripts are located. 75 * This is for both our unique ADT project folder and the user projects folders. 76 */ 77 private static final String FD_GSCRIPTS = "gscripts"; //$NON-NLS-1$ 78 /** 79 * The extension we expect for the groovy scripts. 80 */ 81 private static final String SCRIPT_EXT = ".groovy"; //$NON-NLS-1$ 82 /** 83 * The package we expect for our groovy scripts. 84 * User scripts do not need to use the same (and in fact should probably not.) 85 */ 86 private static final String SCRIPT_PACKAGE = "com.android.adt.gscripts"; //$NON-NLS-1$ 87 88 private final GroovyClassLoader mClassLoader; 89 private final IProject mProject; 90 private final Map<Object, IViewRule> mRulesCache = new HashMap<Object, IViewRule>(); 91 private ProjectFolderListener mProjectFolderListener; 92 93 94 public RulesEngine(IProject project) { 95 mProject = project; 96 ClassLoader cl = getClass().getClassLoader(); 97 98 // Note: we could use the CompilerConfiguration to add an output log collector 99 CompilerConfiguration cc = new CompilerConfiguration(); 100 cc.setDefaultScriptExtension(SCRIPT_EXT); 101 102 mClassLoader = new GreGroovyClassLoader(cl, cc); 103 104 // Add the project's gscript folder to the classpath, if it exists. 105 IResource f = project.findMember(FD_GSCRIPTS); 106 if ((f instanceof IFolder) && f.exists()) { 107 URI uri = ((IFolder) f).getLocationURI(); 108 try { 109 URL url = uri.toURL(); 110 mClassLoader.addURL(url); 111 } catch (MalformedURLException e) { 112 // ignore; it's not a valid URL, we obviously won't use it 113 // in the class path. 114 } 115 } 116 117 mProjectFolderListener = new ProjectFolderListener(); 118 GlobalProjectMonitor.getMonitor().addFolderListener( 119 mProjectFolderListener, 120 IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED); 121 } 122 123 /** 124 * Called by the owner of the {@link RulesEngine} when it is going to be disposed. 125 * This frees some resources, such as the project's folder monitor. 126 */ 127 public void dispose() { 128 if (mProjectFolderListener != null) { 129 GlobalProjectMonitor.getMonitor().removeFolderListener(mProjectFolderListener); 130 mProjectFolderListener = null; 131 } 132 clearCache(); 133 } 134 135 /** 136 * Eventually all rules are going to try to load the base android.view.View rule. 137 * Clients can request to preload it to make the first call faster. 138 */ 139 public void preloadAndroidView() { 140 loadRule(SdkConstants.CLASS_VIEW, SdkConstants.CLASS_VIEW); 141 } 142 143 /** 144 * Invokes {@link IViewRule#getDisplayName()} on the rule matching the specified element. 145 * 146 * @param element The view element to target. Can be null. 147 * @return Null if the rule failed, there's no rule or the rule does not want to override 148 * the display name. Otherwise, a string as returned by the groovy script. 149 */ 150 public String callGetDisplayName(UiViewElementNode element) { 151 // try to find a rule for this element's FQCN 152 IViewRule rule = loadRule(element); 153 154 if (rule != null) { 155 try { 156 return rule.getDisplayName(); 157 158 } catch (Exception e) { 159 logError("%s.getDisplayName() failed: %s", 160 rule.getClass().getSimpleName(), 161 e.toString()); 162 } 163 } 164 165 return null; 166 } 167 168 /** 169 * Invokes {@link IViewRule#onSelected(IGraphics, INode, String, boolean)} 170 * on the rule matching the specified element. 171 * 172 * @param gc An {@link IGraphics} instance, to perform drawing operations. 173 * @param selectedNode The node selected. Never null. 174 * @param displayName The name to display, as returned by {@link IViewRule#getDisplayName()}. 175 * @param isMultipleSelection A boolean set to true if more than one element is selected. 176 */ 177 public void callOnSelected(IGraphics gc, NodeProxy selectedNode, 178 String displayName, boolean isMultipleSelection) { 179 // try to find a rule for this element's FQCN 180 IViewRule rule = loadRule(selectedNode.getNode()); 181 182 if (rule != null) { 183 try { 184 rule.onSelected(gc, selectedNode, displayName, isMultipleSelection); 185 186 } catch (Exception e) { 187 logError("%s.onSelected() failed: %s", 188 rule.getClass().getSimpleName(), 189 e.toString()); 190 } 191 } 192 } 193 194 /** 195 * Invokes {@link IViewRule#onChildSelected(IGraphics, INode, INode)} 196 * on the rule matching the specified element. 197 * 198 * @param gc An {@link IGraphics} instance, to perform drawing operations. 199 * @param parentNode The parent of the node selected. Never null. 200 * @param childNode The child node that was selected. Never null. 201 */ 202 public void callOnChildSelected(IGraphics gc, NodeProxy parentNode, NodeProxy childNode) { 203 // try to find a rule for this element's FQCN 204 IViewRule rule = loadRule(parentNode.getNode()); 205 206 if (rule != null) { 207 try { 208 rule.onChildSelected(gc, parentNode, childNode); 209 210 } catch (Exception e) { 211 logError("%s.onChildSelected() failed: %s", 212 rule.getClass().getSimpleName(), 213 e.toString()); 214 } 215 } 216 } 217 218 219 /** 220 * Called when the d'n'd starts dragging over the target node. 221 * If interested, returns a DropFeedback passed to onDrop/Move/Leave/Paint. 222 * If not interested in drop, return false. 223 * Followed by a paint. 224 */ 225 public DropFeedback callOnDropEnter(NodeProxy targetNode, 226 IDragElement[] elements) { 227 // try to find a rule for this element's FQCN 228 IViewRule rule = loadRule(targetNode.getNode()); 229 230 if (rule != null) { 231 try { 232 return rule.onDropEnter(targetNode, elements); 233 234 } catch (Exception e) { 235 logError("%s.onDropEnter() failed: %s", 236 rule.getClass().getSimpleName(), 237 e.toString()); 238 } 239 } 240 241 return null; 242 } 243 244 /** 245 * Called after onDropEnter. 246 * Returns a DropFeedback passed to onDrop/Move/Leave/Paint (typically same 247 * as input one). 248 */ 249 public DropFeedback callOnDropMove(NodeProxy targetNode, 250 IDragElement[] elements, 251 DropFeedback feedback, 252 Point where) { 253 // try to find a rule for this element's FQCN 254 IViewRule rule = loadRule(targetNode.getNode()); 255 256 if (rule != null) { 257 try { 258 return rule.onDropMove(targetNode, elements, feedback, where); 259 260 } catch (Exception e) { 261 logError("%s.onDropMove() failed: %s", 262 rule.getClass().getSimpleName(), 263 e.toString()); 264 } 265 } 266 267 return null; 268 } 269 270 /** 271 * Called when drop leaves the target without actually dropping 272 */ 273 public void callOnDropLeave(NodeProxy targetNode, 274 IDragElement[] elements, 275 DropFeedback feedback) { 276 // try to find a rule for this element's FQCN 277 IViewRule rule = loadRule(targetNode.getNode()); 278 279 if (rule != null) { 280 try { 281 rule.onDropLeave(targetNode, elements, feedback); 282 283 } catch (Exception e) { 284 logError("%s.onDropLeave() failed: %s", 285 rule.getClass().getSimpleName(), 286 e.toString()); 287 } 288 } 289 } 290 291 /** 292 * Called when drop is released over the target to perform the actual drop. 293 */ 294 public void callOnDropped(NodeProxy targetNode, 295 IDragElement[] elements, 296 DropFeedback feedback, 297 Point where) { 298 // try to find a rule for this element's FQCN 299 IViewRule rule = loadRule(targetNode.getNode()); 300 301 if (rule != null) { 302 try { 303 rule.onDropped(targetNode, elements, feedback, where); 304 305 } catch (Exception e) { 306 logError("%s.onDropped() failed: %s", 307 rule.getClass().getSimpleName(), 308 e.toString()); 309 } 310 } 311 } 312 313 /** 314 * Called when a paint has been requested via DropFeedback. 315 * @param targetNode 316 */ 317 public void callDropFeedbackPaint(IGraphics gc, 318 NodeProxy targetNode, 319 DropFeedback feedback) { 320 if (gc != null && feedback != null && feedback.paintClosure != null) { 321 try { 322 feedback.paintClosure.call(new Object[] { gc, targetNode, feedback }); 323 } catch (Exception e) { 324 logError("DropFeedback.paintClosure failed: %s", 325 e.toString()); 326 } 327 } 328 } 329 330 // ---- private --- 331 332 private class ProjectFolderListener implements IFolderListener { 333 public void folderChanged(IFolder folder, int kind) { 334 if (folder.getProject() == mProject && 335 FD_GSCRIPTS.equals(folder.getName())) { 336 // Clear our whole rules cache, to not have to deal with dependencies. 337 clearCache(); 338 } 339 } 340 } 341 342 /** 343 * Clear the Rules cache. Calls onDispose() on each rule. 344 */ 345 private void clearCache() { 346 // The cache can contain multiple times the same rule instance for different 347 // keys (e.g. the UiViewElementNode key vs. the FQCN string key.) So transfer 348 // all values to a unique set. 349 HashSet<IViewRule> rules = new HashSet<IViewRule>(mRulesCache.values()); 350 351 mRulesCache.clear(); 352 353 for (IViewRule rule : rules) { 354 if (rule != null) { 355 try { 356 rule.onDispose(); 357 } catch (Exception e) { 358 logError("%s.onDispose() failed: %s", 359 rule.getClass().getSimpleName(), 360 e.toString()); 361 } 362 } 363 } 364 } 365 366 /** 367 * Load a rule using its descriptor. This will try to first load the rule using its 368 * actual FQCN and if that fails will find the first parent that works in the view 369 * hierarchy. 370 */ 371 private IViewRule loadRule(UiViewElementNode element) { 372 if (element == null) { 373 return null; 374 } else { 375 // sanity check. this can't fail. 376 ElementDescriptor d = element.getDescriptor(); 377 if (d == null || !(d instanceof ViewElementDescriptor)) { 378 return null; 379 } 380 } 381 382 String targetFqcn = null; 383 ViewElementDescriptor targetDesc = (ViewElementDescriptor) element.getDescriptor(); 384 385 // Return the rule if we find it in the cache, even if it was stored as null 386 // (which means we didn't find it earlier, so don't look for it again) 387 IViewRule rule = mRulesCache.get(targetDesc); 388 if (rule != null || mRulesCache.containsKey(targetDesc)) { 389 return rule; 390 } 391 392 // Get the descriptor and loop through the super class hierarchy 393 for (ViewElementDescriptor desc = targetDesc; 394 desc != null; 395 desc = desc.getSuperClassDesc()) { 396 397 // Get the FQCN of this View 398 String fqcn = desc.getFullClassName(); 399 if (fqcn == null) { 400 return null; 401 } 402 403 // The first time we keep the FQCN around as it's the target class we were 404 // initially trying to load. After, as we move through the hierarchy, the 405 // target FQCN remains constant. 406 if (targetFqcn == null) { 407 targetFqcn = fqcn; 408 } 409 410 // Try to find a rule matching the "real" FQCN. If we find it, we're done. 411 // If not, the for loop will move to the parent descriptor. 412 rule = loadRule(fqcn, targetFqcn); 413 if (rule != null) { 414 // We found one. 415 // As a side effect, loadRule() also cached the rule using the target FQCN. 416 return rule; 417 } 418 } 419 420 // Memorize in the cache that we couldn't find a rule for this descriptor 421 mRulesCache.put(targetDesc, null); 422 return null; 423 } 424 425 /** 426 * Try to load a rule given a specific FQCN. This looks for an exact match in either 427 * the ADT scripts or the project scripts and does not look at parent hierarchy. 428 * <p/> 429 * Once a rule is found (or not), it is stored in a cache using its target FQCN 430 * so we don't try to reload it. 431 * <p/> 432 * The real FQCN is the actual groovy filename we're loading, e.g. "android.view.View.groovy" 433 * where target FQCN is the class we were initially looking for, which might be the same as 434 * the real FQCN or might be a derived class, e.g. "android.widget.TextView". 435 * 436 * @param realFqcn The FQCN of the groovy rule actually being loaded. 437 * @param targetFqcn The FQCN of the class actually processed, which might be different from 438 * the FQCN of the rule being loaded. 439 */ 440 private IViewRule loadRule(String realFqcn, String targetFqcn) { 441 if (realFqcn == null || targetFqcn == null) { 442 return null; 443 } 444 445 // Return the rule if we find it in the cache, even if it was stored as null 446 // (which means we didn't find it earlier, so don't look for it again) 447 IViewRule rule = mRulesCache.get(realFqcn); 448 if (rule != null || mRulesCache.containsKey(realFqcn)) { 449 return rule; 450 } 451 452 // Look for the file in ADT first. 453 // That means a project can't redefine any of the rules we define. 454 String filename = realFqcn + SCRIPT_EXT; 455 456 try { 457 InputStream is = AdtPlugin.readEmbeddedFileAsStream( 458 FD_GSCRIPTS + AndroidConstants.WS_SEP + filename); 459 rule = loadStream(is, realFqcn, "ADT"); //$NON-NLS-1$ 460 if (rule != null) { 461 return initializeRule(rule, targetFqcn); 462 } 463 } catch (Exception e) { 464 logError("load rule error (%s): %s", filename, e.toString()); 465 } 466 467 468 // Then look for the file in the project 469 IResource r = mProject.findMember(FD_GSCRIPTS); 470 if (r != null && r.getType() == IResource.FOLDER) { 471 r = ((IFolder) r).findMember(filename); 472 if (r != null && r.getType() == IResource.FILE) { 473 try { 474 InputStream is = ((IFile) r).getContents(); 475 rule = loadStream(is, realFqcn, mProject.getName()); 476 if (rule != null) { 477 return initializeRule(rule, targetFqcn); 478 } 479 } catch (Exception e) { 480 logError("load rule error (%s): %s", filename, e.getMessage()); 481 } 482 } 483 } 484 485 // Memorize in the cache that we couldn't find a rule for this real FQCN 486 mRulesCache.put(realFqcn, null); 487 return null; 488 } 489 490 /** 491 * Initialize a rule we just loaded. The rule has a chance to examine the target FQCN 492 * and bail out. 493 * <p/> 494 * Contract: the rule is not in the {@link #mRulesCache} yet and this method will 495 * cache it using the target FQCN if the rule is accepted. 496 * <p/> 497 * The real FQCN is the actual groovy filename we're loading, e.g. "android.view.View.groovy" 498 * where target FQCN is the class we were initially looking for, which might be the same as 499 * the real FQCN or might be a derived class, e.g. "android.widget.TextView". 500 * 501 * @param rule A rule freshly loaded. 502 * @param targetFqcn The FQCN of the class actually processed, which might be different from 503 * the FQCN of the rule being loaded. 504 * @return The rule if accepted, or null if the rule can't handle that FQCN. 505 */ 506 private IViewRule initializeRule(IViewRule rule, String targetFqcn) { 507 508 try { 509 if (rule.onInitialize(targetFqcn)) { 510 // Add it to the cache and return it 511 mRulesCache.put(targetFqcn, rule); 512 return rule; 513 } else { 514 rule.onDispose(); 515 } 516 } catch (Exception e) { 517 logError("%s.onInit() failed: %s", 518 rule.getClass().getSimpleName(), 519 e.toString()); 520 } 521 522 return null; 523 } 524 525 /** 526 * Actually load a groovy script and instantiate an {@link IViewRule} from it. 527 * On error, outputs (hopefully meaningful) groovy error messages. 528 * 529 * @param is The input stream for the groovy script. Can be null. 530 * @param fqcn The class name, for display purposes only. 531 * @param codeBase A string eventually passed to {@link CodeSource} to define some kind 532 * of security permission. Quite irrelevant in our case since it all 533 * comes from an input stream. However this method uses it to print 534 * the origin of the source in the exception errors. 535 * @return A new {@link IViewRule} or null if loading failed for any reason. 536 */ 537 private IViewRule loadStream(InputStream is, String fqcn, String codeBase) { 538 try { 539 if (is == null) { 540 // We handle this case for convenience. It typically means that the 541 // input stream couldn't be opened because the file was not found. 542 // Since we expect this to be a common case, we don't log it as an error. 543 return null; 544 } 545 546 // We don't really now the character encoding, we're going to assume UTF-8. 547 InputStreamReader reader = new InputStreamReader(is, Charset.forName("UTF-8")); 548 GroovyCodeSource source = new GroovyCodeSource(reader, fqcn, codeBase); 549 550 // Create a groovy class from it. Can fail to compile. 551 Class<?> c = mClassLoader.parseClass(source); 552 553 // Get an instance. This might throw ClassCastException. 554 return (IViewRule) c.newInstance(); 555 556 } catch (CompilationFailedException e) { 557 logError("Compilation error in %1$s:%2$s.groovy: %3$s", codeBase, fqcn, e.toString()); 558 } catch (ClassCastException e) { 559 logError("Script %1$s:%2$s.groovy does not implement IViewRule", codeBase, fqcn); 560 } catch (Exception e) { 561 logError("Failed to use %1$s:%2$s.groovy: %3$s", codeBase, fqcn, e.toString()); 562 } 563 564 return null; 565 } 566 567 private void logError(String format, Object...params) { 568 String s = String.format(format, params); 569 AdtPlugin.printErrorToConsole(mProject, s); 570 } 571 572 // ----- 573 574 /** 575 * A custom {@link GroovyClassLoader} that lets us override the {@link CompilationUnit} 576 * and the {@link GroovyResourceLoader}. 577 */ 578 private static class GreGroovyClassLoader extends GroovyClassLoader { 579 580 public GreGroovyClassLoader(ClassLoader cl, CompilerConfiguration cc) { 581 super(cl, cc); 582 583 // Override the resource loader: when a class is not found, we try to find a class 584 // defined in our internal ADT groovy script, assuming it has our special package. 585 // Note that these classes do not have to implement IViewRule. That means we can 586 // create utility classes in groovy used by the other groovy rules. 587 final GroovyResourceLoader resLoader = getResourceLoader(); 588 setResourceLoader(new GroovyResourceLoader() { 589 public URL loadGroovySource(String filename) throws MalformedURLException { 590 URL url = resLoader.loadGroovySource(filename); 591 if (url == null) { 592 // We only try to load classes in our own groovy script package 593 String p = SCRIPT_PACKAGE + "."; //$NON-NLS-1$ 594 595 if (filename.startsWith(p)) { 596 filename = filename.substring(p.length()); 597 598 // This will return null if the file doesn't exists. 599 // The groovy resolver will actually load and verify the class 600 // implemented matches the one it was expecting in the first place, 601 // so we don't have anything to do here besides returning the URL to 602 // the source file. 603 url = AdtPlugin.getEmbeddedFileUrl( 604 AndroidConstants.WS_SEP + 605 FD_GSCRIPTS + 606 AndroidConstants.WS_SEP + 607 filename + 608 SCRIPT_EXT); 609 } 610 } 611 return url; 612 } 613 }); 614 } 615 616 @Override 617 protected CompilationUnit createCompilationUnit( 618 CompilerConfiguration config, 619 CodeSource source) { 620 return new GreCompilationUnit(config, source, this); 621 } 622 } 623 624 /** 625 * A custom {@link CompilationUnit} that lets us add default import for our base classes 626 * using the base package of {@link IViewRule} (e.g. "import com.android...gscripts.*") 627 */ 628 private static class GreCompilationUnit extends CompilationUnit { 629 630 public GreCompilationUnit( 631 CompilerConfiguration config, 632 CodeSource source, 633 GroovyClassLoader loader) { 634 super(config, source, loader); 635 636 SourceUnitOperation op = new SourceUnitOperation() { 637 @Override 638 public void call(SourceUnit source) throws CompilationFailedException { 639 // add the equivalent of "import com.android...gscripts.*" to the source. 640 String p = IViewRule.class.getPackage().getName(); 641 source.getAST().addStarImport(p + "."); //$NON-NLS-1$ 642 } 643 }; 644 645 addPhaseOperation(op, Phases.CONVERSION); 646 } 647 } 648 649 } 650