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