1 /* 2 * Copyright (C) 2010 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.build; 18 19 import com.android.ide.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 22 23 import org.eclipse.core.resources.IFile; 24 import org.eclipse.core.resources.IMarker; 25 import org.eclipse.core.resources.IProject; 26 import org.eclipse.core.resources.IResource; 27 import org.eclipse.core.runtime.CoreException; 28 import org.eclipse.jface.text.FindReplaceDocumentAdapter; 29 import org.eclipse.jface.text.IDocument; 30 import org.eclipse.jface.text.IRegion; 31 import org.eclipse.jface.text.Region; 32 import org.eclipse.ui.editors.text.TextFileDocumentProvider; 33 import org.eclipse.ui.texteditor.IDocumentProvider; 34 35 import java.io.File; 36 import java.util.List; 37 import java.util.regex.Matcher; 38 import java.util.regex.Pattern; 39 40 public final class AaptParser { 41 42 // TODO: rename the pattern to something that makes sense + javadoc comments. 43 /** 44 * Single line aapt warning for skipping files.<br> 45 * " (skipping hidden file '<file path>'" 46 */ 47 private final static Pattern sPattern0Line1 = Pattern.compile( 48 "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$ 49 50 /** 51 * First line of dual line aapt error.<br> 52 * "ERROR at line <line>: <error>"<br> 53 * " (Occurred while parsing <path>)" 54 */ 55 private final static Pattern sPattern1Line1 = Pattern.compile( 56 "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$ 57 /** 58 * Second line of dual line aapt error.<br> 59 * "ERROR at line <line>: <error>"<br> 60 * " (Occurred while parsing <path>)"<br> 61 * @see #sPattern1Line1 62 */ 63 private final static Pattern sPattern1Line2 = Pattern.compile( 64 "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$ 65 /** 66 * First line of dual line aapt error.<br> 67 * "ERROR: <error>"<br> 68 * "Defined at file <path> line <line>" 69 */ 70 private final static Pattern sPattern2Line1 = Pattern.compile( 71 "^ERROR:\\s+(.+)$"); //$NON-NLS-1$ 72 /** 73 * Second line of dual line aapt error.<br> 74 * "ERROR: <error>"<br> 75 * "Defined at file <path> line <line>"<br> 76 * @see #sPattern2Line1 77 */ 78 private final static Pattern sPattern2Line2 = Pattern.compile( 79 "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$ 80 /** 81 * Single line aapt error<br> 82 * "<path> line <line>: <error>" 83 */ 84 private final static Pattern sPattern3Line1 = Pattern.compile( 85 "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$ 86 /** 87 * First line of dual line aapt error.<br> 88 * "ERROR parsing XML file <path>"<br> 89 * "<error> at line <line>" 90 */ 91 private final static Pattern sPattern4Line1 = Pattern.compile( 92 "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$ 93 /** 94 * Second line of dual line aapt error.<br> 95 * "ERROR parsing XML file <path>"<br> 96 * "<error> at line <line>"<br> 97 * @see #sPattern4Line1 98 */ 99 private final static Pattern sPattern4Line2 = Pattern.compile( 100 "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$ 101 102 /** 103 * Single line aapt warning<br> 104 * "<path>:<line>: <error>" 105 */ 106 private final static Pattern sPattern5Line1 = Pattern.compile( 107 "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$ 108 109 /** 110 * Single line aapt error<br> 111 * "<path>:<line>: <error>" 112 */ 113 private final static Pattern sPattern6Line1 = Pattern.compile( 114 "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$ 115 116 /** 117 * 4 line aapt error<br> 118 * "ERROR: 9-path image <path> malformed"<br> 119 * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br> 120 * 'ERROR: failure processing <path>) 121 */ 122 private final static Pattern sPattern7Line1 = Pattern.compile( 123 "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$ 124 125 private final static Pattern sPattern8Line1 = Pattern.compile( 126 "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$ 127 128 /** 129 * Portion of the error message which states the context in which the error occurred, 130 * such as which property was being processed and what the string value was that 131 * caused the error. 132 * <p> 133 * Example: 134 * error: No resource found that matches the given name (at 'text' with value '@string/foo') 135 */ 136 private static final Pattern sValueRangePattern = 137 Pattern.compile("\\(at '(.+)' with value '(.*)'\\)"); //$NON-NLS-1$ 138 139 140 /** 141 * Portion of error message which points to the second occurrence of a repeated resource 142 * definition. 143 * <p> 144 * Example: 145 * error: Resource entry repeatedStyle1 already has bag item android:gravity. 146 */ 147 private static final Pattern sRepeatedRangePattern = 148 Pattern.compile("Resource entry (.+) already has bag item (.+)\\."); //$NON-NLS-1$ 149 150 /** 151 * Error message emitted when aapt skips a file because for example it's name is 152 * invalid, such as a layout file name which starts with _. 153 */ 154 private static final Pattern sSkippingPattern = 155 Pattern.compile(" \\(skipping .+ .+ '(.*)'\\)"); //$NON-NLS-1$ 156 157 /** 158 * Suffix of error message which points to the first occurrence of a repeated resource 159 * definition. 160 * Example: 161 * Originally defined here. 162 */ 163 private static final String ORIGINALLY_DEFINED_MSG = "Originally defined here."; //$NON-NLS-1$ 164 165 /** 166 * Portion of error message which points to the second occurrence of a repeated resource 167 * definition. 168 * <p> 169 * Example: 170 * error: Resource entry repeatedStyle1 already has bag item android:gravity. 171 */ 172 private static final Pattern sNoResourcePattern = 173 Pattern.compile("No resource found that matches the given name: attr '(.+)'\\."); //$NON-NLS-1$ 174 175 /** 176 * Portion of error message which points to a missing required attribute in a 177 * resource definition. 178 * <p> 179 * Example: 180 * error: error: A 'name' attribute is required for <style> 181 */ 182 private static final Pattern sRequiredPattern = 183 Pattern.compile("A '(.+)' attribute is required for <(.+)>"); //$NON-NLS-1$ 184 185 /** 186 * 2 line aapt error<br> 187 * "ERROR: Invalid configuration: foo"<br> 188 * " ^^^"<br> 189 * There's no need to parse the 2nd line. 190 */ 191 private final static Pattern sPattern9Line1 = Pattern.compile( 192 "^Invalid configuration: (.+)$"); //$NON-NLS-1$ 193 194 /** 195 * Parse the output of aapt and mark the incorrect file with error markers 196 * 197 * @param results the output of aapt 198 * @param project the project containing the file to mark 199 * @return true if the parsing failed, false if success. 200 */ parseOutput(List<String> results, IProject project)201 public static boolean parseOutput(List<String> results, IProject project) { 202 int size = results.size(); 203 if (size > 0) { 204 return parseOutput(results.toArray(new String[size]), project); 205 } 206 207 return false; 208 } 209 210 /** 211 * Parse the output of aapt and mark the incorrect file with error markers 212 * 213 * @param results the output of aapt 214 * @param project the project containing the file to mark 215 * @return true if the parsing failed, false if success. 216 */ parseOutput(String[] results, IProject project)217 public static boolean parseOutput(String[] results, IProject project) { 218 // nothing to parse? just return false; 219 if (results.length == 0) { 220 return false; 221 } 222 223 // get the root of the project so that we can make IFile from full 224 // file path 225 String osRoot = project.getLocation().toOSString(); 226 227 Matcher m; 228 229 for (int i = 0; i < results.length ; i++) { 230 String p = results[i]; 231 232 m = sPattern0Line1.matcher(p); 233 if (m.matches()) { 234 // we ignore those (as this is an ignore message from aapt) 235 continue; 236 } 237 238 m = sPattern1Line1.matcher(p); 239 if (m.matches()) { 240 String lineStr = m.group(1); 241 String msg = m.group(2); 242 243 // get the matcher for the next line. 244 m = getNextLineMatcher(results, ++i, sPattern1Line2); 245 if (m == null) { 246 return true; 247 } 248 249 String location = m.group(1); 250 251 // check the values and attempt to mark the file. 252 if (checkAndMark(location, lineStr, msg, osRoot, project, 253 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 254 return true; 255 } 256 continue; 257 } 258 259 // this needs to be tested before Pattern2 since they both start with 'ERROR:' 260 m = sPattern7Line1.matcher(p); 261 if (m.matches()) { 262 String location = m.group(1); 263 String msg = p; // default msg is the line in case we don't find anything else 264 265 if (++i < results.length) { 266 msg = results[i].trim(); 267 if (++i < results.length) { 268 msg = msg + " - " + results[i].trim(); //$NON-NLS-1$ 269 270 // skip the next line 271 i++; 272 } 273 } 274 275 // display the error 276 if (checkAndMark(location, null, msg, osRoot, project, 277 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 278 return true; 279 } 280 281 // success, go to the next line 282 continue; 283 } 284 285 m = sPattern2Line1.matcher(p); 286 if (m.matches()) { 287 // get the msg 288 String msg = m.group(1); 289 290 // get the matcher for the next line. 291 m = getNextLineMatcher(results, ++i, sPattern2Line2); 292 if (m == null) { 293 return true; 294 } 295 296 String location = m.group(1); 297 String lineStr = m.group(2); 298 299 // check the values and attempt to mark the file. 300 if (checkAndMark(location, lineStr, msg, osRoot, project, 301 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 302 return true; 303 } 304 continue; 305 } 306 307 m = sPattern3Line1.matcher(p); 308 if (m.matches()) { 309 String location = m.group(1); 310 String lineStr = m.group(2); 311 String msg = m.group(3); 312 313 // check the values and attempt to mark the file. 314 if (checkAndMark(location, lineStr, msg, osRoot, project, 315 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 316 return true; 317 } 318 319 // success, go to the next line 320 continue; 321 } 322 323 m = sPattern4Line1.matcher(p); 324 if (m.matches()) { 325 // get the filename. 326 String location = m.group(1); 327 328 // get the matcher for the next line. 329 m = getNextLineMatcher(results, ++i, sPattern4Line2); 330 if (m == null) { 331 return true; 332 } 333 334 String msg = m.group(1); 335 String lineStr = m.group(2); 336 337 // check the values and attempt to mark the file. 338 if (checkAndMark(location, lineStr, msg, osRoot, project, 339 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 340 return true; 341 } 342 343 // success, go to the next line 344 continue; 345 } 346 347 m = sPattern5Line1.matcher(p); 348 if (m.matches()) { 349 String location = m.group(1); 350 String lineStr = m.group(2); 351 String msg = m.group(3); 352 353 // check the values and attempt to mark the file. 354 if (checkAndMark(location, lineStr, msg, osRoot, project, 355 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) { 356 return true; 357 } 358 359 // success, go to the next line 360 continue; 361 } 362 363 m = sPattern6Line1.matcher(p); 364 if (m.matches()) { 365 String location = m.group(1); 366 String lineStr = m.group(2); 367 String msg = m.group(3); 368 369 // check the values and attempt to mark the file. 370 if (checkAndMark(location, lineStr, msg, osRoot, project, 371 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 372 return true; 373 } 374 375 // success, go to the next line 376 continue; 377 } 378 379 m = sPattern8Line1.matcher(p); 380 if (m.matches()) { 381 String location = m.group(2); 382 String msg = m.group(1); 383 384 // check the values and attempt to mark the file. 385 if (checkAndMark(location, null, msg, osRoot, project, 386 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 387 return true; 388 } 389 390 // success, go to the next line 391 continue; 392 } 393 394 m = sPattern9Line1.matcher(p); 395 if (m.matches()) { 396 String badConfig = m.group(1); 397 String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig); 398 399 // skip the next line 400 i++; 401 402 // check the values and attempt to mark the file. 403 if (checkAndMark(null /*location*/, null, msg, osRoot, project, 404 AdtConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) { 405 return true; 406 } 407 408 // success, go to the next line 409 continue; 410 } 411 412 m = sSkippingPattern.matcher(p); 413 if (m.matches()) { 414 String location = m.group(1); 415 416 // check the values and attempt to mark the file. 417 if (checkAndMark(location, null, p.trim(), osRoot, project, 418 AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) { 419 return true; 420 } 421 422 // success, go to the next line 423 continue; 424 } 425 426 // invalid line format, flag as error, and bail 427 return true; 428 } 429 430 return false; 431 } 432 433 /** 434 * Check if the parameters gotten from the error output are valid, and mark 435 * the file with an AAPT marker. 436 * @param location the full OS path of the error file. If null, the project is marked 437 * @param lineStr 438 * @param message 439 * @param root The root directory of the project, in OS specific format. 440 * @param project 441 * @param markerId The marker id to put. 442 * @param severity The severity of the marker to put (IMarker.SEVERITY_*) 443 * @return true if the parameters were valid and the file was marked successfully. 444 * 445 * @see IMarker 446 */ checkAndMark(String location, String lineStr, String message, String root, IProject project, String markerId, int severity)447 private static final boolean checkAndMark(String location, String lineStr, 448 String message, String root, IProject project, String markerId, int severity) { 449 // check this is in fact a file 450 if (location != null) { 451 File f = new File(location); 452 if (f.exists() == false) { 453 return false; 454 } 455 } 456 457 // get the line number 458 int line = -1; // default value for error with no line. 459 460 if (lineStr != null) { 461 try { 462 line = Integer.parseInt(lineStr); 463 } catch (NumberFormatException e) { 464 // looks like the string we extracted wasn't a valid 465 // file number. Parsing failed and we return true 466 return false; 467 } 468 } 469 470 // add the marker 471 IResource f2 = project; 472 if (location != null) { 473 f2 = getResourceFromFullPath(location, root, project); 474 if (f2 == null) { 475 return false; 476 } 477 } 478 479 // Attempt to determine the exact range of characters affected by this error. 480 // This will look up the actual text of the file, go to the particular error line 481 // and scan for the specific string mentioned in the error. 482 int startOffset = -1; 483 int endOffset = -1; 484 if (f2 instanceof IFile) { 485 IRegion region = findRange((IFile) f2, line, message); 486 if (region != null) { 487 startOffset = region.getOffset(); 488 endOffset = startOffset + region.getLength(); 489 } 490 } 491 492 // check if there's a similar marker already, since aapt is launched twice 493 boolean markerAlreadyExists = false; 494 try { 495 IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO); 496 497 for (IMarker marker : markers) { 498 if (startOffset != -1) { 499 int tmpBegin = marker.getAttribute(IMarker.CHAR_START, -1); 500 if (tmpBegin != startOffset) { 501 break; 502 } 503 int tmpEnd = marker.getAttribute(IMarker.CHAR_END, -1); 504 if (tmpEnd != startOffset) { 505 break; 506 } 507 } 508 509 int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1); 510 if (tmpLine != line) { 511 break; 512 } 513 514 int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1); 515 if (tmpSeverity != severity) { 516 break; 517 } 518 519 String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null); 520 if (tmpMsg == null || tmpMsg.equals(message) == false) { 521 break; 522 } 523 524 // if we're here, all the marker attributes are equals, we found it 525 // and exit 526 markerAlreadyExists = true; 527 break; 528 } 529 530 } catch (CoreException e) { 531 // if we couldn't get the markers, then we just mark the file again 532 // (since markerAlreadyExists is initialized to false, we do nothing) 533 } 534 535 if (markerAlreadyExists == false) { 536 BaseProjectHelper.markResource(f2, markerId, message, line, 537 startOffset, endOffset, severity); 538 } 539 540 return true; 541 } 542 543 /** 544 * Given an aapt error message in a given file and a given (initial) line number, 545 * return the corresponding offset range for the error, or null. 546 */ findRange(IFile file, int line, String message)547 private static IRegion findRange(IFile file, int line, String message) { 548 Matcher matcher = sValueRangePattern.matcher(message); 549 if (matcher.find()) { 550 String property = matcher.group(1); 551 String value = matcher.group(2); 552 553 // First find the property. We can't just immediately look for the 554 // value, because there could be other attributes in this element 555 // earlier than the one in error, and we might accidentally pick 556 // up on a different occurrence of the value in a context where 557 // it is valid. 558 if (value.length() > 0) { 559 return findRange(file, line, property, value); 560 } else { 561 // Find first occurrence of property followed by '' or "" 562 IRegion region1 = findRange(file, line, property, "\"\""); //$NON-NLS-1$ 563 IRegion region2 = findRange(file, line, property, "''"); //$NON-NLS-1$ 564 if (region1 == null) { 565 if (region2 == null) { 566 // Highlight the property instead 567 return findRange(file, line, property, null); 568 } 569 return region2; 570 } else if (region2 == null) { 571 return region1; 572 } else if (region1.getOffset() < region2.getOffset()) { 573 return region1; 574 } else { 575 return region2; 576 } 577 } 578 } 579 580 matcher = sRepeatedRangePattern.matcher(message); 581 if (matcher.find()) { 582 String property = matcher.group(2); 583 return findRange(file, line, property, null); 584 } 585 586 matcher = sNoResourcePattern.matcher(message); 587 if (matcher.find()) { 588 String property = matcher.group(1); 589 return findRange(file, line, property, null); 590 } 591 592 matcher = sRequiredPattern.matcher(message); 593 if (matcher.find()) { 594 String elementName = matcher.group(2); 595 IRegion region = findRange(file, line, '<' + elementName, null); 596 if (region != null && region.getLength() > 1) { 597 // Skip the opening < 598 region = new Region(region.getOffset() + 1, region.getLength() - 1); 599 } 600 return region; 601 } 602 603 if (message.endsWith(ORIGINALLY_DEFINED_MSG)) { 604 return findLineTextRange(file, line); 605 } 606 607 return null; 608 } 609 610 /** 611 * Given a file and line number, return the range of the first match starting on the 612 * given line. If second is non null, also search for the second string starting at he 613 * location of the first string. 614 */ findRange(IFile file, int line, String first, String second)615 private static IRegion findRange(IFile file, int line, String first, 616 String second) { 617 IRegion region = null; 618 IDocumentProvider provider = new TextFileDocumentProvider(); 619 try { 620 provider.connect(file); 621 IDocument document = provider.getDocument(file); 622 if (document != null) { 623 IRegion lineInfo = document.getLineInformation(line - 1); 624 int lineStartOffset = lineInfo.getOffset(); 625 // The aapt errors will be anchored on the line where the 626 // element starts - which means that with formatting where 627 // attributes end up on subsequent lines we don't find it on 628 // the error line indicated by aapt. 629 // Therefore, search forwards in the document. 630 FindReplaceDocumentAdapter adapter = 631 new FindReplaceDocumentAdapter(document); 632 633 region = adapter.find(lineStartOffset, first, 634 true /*forwardSearch*/, true /*caseSensitive*/, 635 false /*wholeWord*/, false /*regExSearch*/); 636 if (region != null && second != null) { 637 region = adapter.find(region.getOffset() + first.length(), second, 638 true /*forwardSearch*/, true /*caseSensitive*/, 639 false /*wholeWord*/, false /*regExSearch*/); 640 } 641 } 642 } catch (Exception e) { 643 AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); 644 } finally { 645 provider.disconnect(file); 646 } 647 return region; 648 } 649 650 /** Returns the non-whitespace line range at the given line number. */ findLineTextRange(IFile file, int line)651 private static IRegion findLineTextRange(IFile file, int line) { 652 IDocumentProvider provider = new TextFileDocumentProvider(); 653 try { 654 provider.connect(file); 655 IDocument document = provider.getDocument(file); 656 if (document != null) { 657 IRegion lineInfo = document.getLineInformation(line - 1); 658 String lineContents = document.get(lineInfo.getOffset(), lineInfo.getLength()); 659 int lineBegin = 0; 660 int lineEnd = lineContents.length()-1; 661 662 for (; lineEnd >= 0; lineEnd--) { 663 char c = lineContents.charAt(lineEnd); 664 if (!Character.isWhitespace(c)) { 665 break; 666 } 667 } 668 lineEnd++; 669 for (; lineBegin < lineEnd; lineBegin++) { 670 char c = lineContents.charAt(lineBegin); 671 if (!Character.isWhitespace(c)) { 672 break; 673 } 674 } 675 if (lineBegin < lineEnd) { 676 return new Region(lineInfo.getOffset() + lineBegin, lineEnd - lineBegin); 677 } 678 } 679 } catch (Exception e) { 680 AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); 681 } finally { 682 provider.disconnect(file); 683 } 684 685 return null; 686 } 687 688 /** 689 * Returns a matching matcher for the next line 690 * @param lines The array of lines 691 * @param nextIndex The index of the next line 692 * @param pattern The pattern to match 693 * @return null if error or no match, the matcher otherwise. 694 */ getNextLineMatcher(String[] lines, int nextIndex, Pattern pattern)695 private static final Matcher getNextLineMatcher(String[] lines, 696 int nextIndex, Pattern pattern) { 697 // unless we can't, because we reached the last line 698 if (nextIndex == lines.length) { 699 // we expected a 2nd line, so we flag as error 700 // and we bail 701 return null; 702 } 703 704 Matcher m = pattern.matcher(lines[nextIndex]); 705 if (m.matches()) { 706 return m; 707 } 708 709 return null; 710 } 711 getResourceFromFullPath(String filename, String root, IProject project)712 private static IResource getResourceFromFullPath(String filename, String root, 713 IProject project) { 714 if (filename.startsWith(root)) { 715 String file = filename.substring(root.length()); 716 717 // get the resource 718 IResource r = project.findMember(file); 719 720 // if the resource is valid, we add the marker 721 if (r != null && r.exists()) { 722 return r; 723 } 724 } 725 726 return null; 727 } 728 729 } 730