1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 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.ant; 18 19 import com.android.io.FileWrapper; 20 import com.android.io.FolderWrapper; 21 import com.android.sdklib.AndroidVersion; 22 import com.android.sdklib.IAndroidTarget; 23 import com.android.sdklib.ISdkLog; 24 import com.android.sdklib.SdkConstants; 25 import com.android.sdklib.SdkManager; 26 import com.android.sdklib.IAndroidTarget.IOptionalLibrary; 27 import com.android.sdklib.internal.project.ProjectProperties; 28 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 29 import com.android.sdklib.xml.AndroidManifest; 30 import com.android.sdklib.xml.AndroidXPathFactory; 31 32 import org.apache.tools.ant.BuildException; 33 import org.apache.tools.ant.Project; 34 import org.apache.tools.ant.Task; 35 import org.apache.tools.ant.types.Path; 36 import org.apache.tools.ant.types.Path.PathElement; 37 import org.apache.tools.ant.util.DeweyDecimal; 38 import org.xml.sax.InputSource; 39 40 import java.io.File; 41 import java.io.FileInputStream; 42 import java.io.FileNotFoundException; 43 import java.io.FilenameFilter; 44 import java.io.IOException; 45 import java.util.ArrayList; 46 import java.util.HashSet; 47 import java.util.List; 48 49 import javax.xml.xpath.XPath; 50 import javax.xml.xpath.XPathExpressionException; 51 52 /** 53 * Setup Ant task. This task accomplishes: 54 * <ul> 55 * <li>Gets the project target hash string from {@link ProjectProperties#PROPERTY_TARGET}, 56 * and resolves it to get the project's {@link IAndroidTarget}.</li> 57 * 58 * <li>Sets up properties so that aapt can find the android.jar and other files/folders in 59 * the resolved target.</li> 60 * 61 * <li>Sets up the boot classpath ref so that the <code>javac</code> task knows where to find 62 * the libraries. This includes the default android.jar from the resolved target but also optional 63 * libraries provided by the target (if any, when the target is an add-on).</li> 64 * 65 * <li>Resolve library dependencies and setup various Path references for them</li> 66 * </ul> 67 * 68 * This is used in the main rules file only. 69 * 70 */ 71 public class NewSetupTask extends Task { 72 private final static String ANT_MIN_VERSION = "1.8.0"; 73 74 private String mProjectTypeOut; 75 private String mAndroidJarFileOut; 76 private String mAndroidAidlFileOut; 77 private String mRenderScriptExeOut; 78 private String mRenderScriptIncludeDirOut; 79 private String mBootclasspathrefOut; 80 private String mProjectLibrariesRootOut; 81 private String mProjectLibrariesResOut; 82 private String mProjectLibrariesPackageOut; 83 private String mProjectLibrariesJarsOut; 84 private String mProjectLibrariesLibsOut; 85 private String mTargetApiOut; 86 87 public void setProjectTypeOut(String projectTypeOut) { 88 mProjectTypeOut = projectTypeOut; 89 } 90 91 public void setAndroidJarFileOut(String androidJarFileOut) { 92 mAndroidJarFileOut = androidJarFileOut; 93 } 94 95 public void setAndroidAidlFileOut(String androidAidlFileOut) { 96 mAndroidAidlFileOut = androidAidlFileOut; 97 } 98 99 public void setRenderScriptExeOut(String renderScriptExeOut) { 100 mRenderScriptExeOut = renderScriptExeOut; 101 } 102 103 public void setRenderScriptIncludeDirOut(String renderScriptIncludeDirOut) { 104 mRenderScriptIncludeDirOut = renderScriptIncludeDirOut; 105 } 106 107 public void setBootclasspathrefOut(String bootclasspathrefOut) { 108 mBootclasspathrefOut = bootclasspathrefOut; 109 } 110 111 public void setProjectLibrariesRootOut(String projectLibrariesRootOut) { 112 mProjectLibrariesRootOut = projectLibrariesRootOut; 113 } 114 115 public void setProjectLibrariesResOut(String projectLibrariesResOut) { 116 mProjectLibrariesResOut = projectLibrariesResOut; 117 } 118 119 public void setProjectLibrariesPackageOut(String projectLibrariesPackageOut) { 120 mProjectLibrariesPackageOut = projectLibrariesPackageOut; 121 } 122 123 public void setProjectLibrariesJarsOut(String projectLibrariesJarsOut) { 124 mProjectLibrariesJarsOut = projectLibrariesJarsOut; 125 } 126 127 public void setProjectLibrariesLibsOut(String projectLibrariesLibsOut) { 128 mProjectLibrariesLibsOut = projectLibrariesLibsOut; 129 } 130 131 public void setTargetApiOut(String targetApiOut) { 132 mTargetApiOut = targetApiOut; 133 } 134 135 @Override 136 public void execute() throws BuildException { 137 if (mProjectTypeOut == null) { 138 throw new BuildException("Missing attribute projectTypeOut"); 139 } 140 if (mAndroidJarFileOut == null) { 141 throw new BuildException("Missing attribute androidJarFileOut"); 142 } 143 if (mAndroidAidlFileOut == null) { 144 throw new BuildException("Missing attribute androidAidlFileOut"); 145 } 146 if (mRenderScriptExeOut == null) { 147 throw new BuildException("Missing attribute renderScriptExeOut"); 148 } 149 if (mRenderScriptIncludeDirOut == null) { 150 throw new BuildException("Missing attribute renderScriptIncludeDirOut"); 151 } 152 if (mBootclasspathrefOut == null) { 153 throw new BuildException("Missing attribute bootclasspathrefOut"); 154 } 155 if (mProjectLibrariesRootOut == null) { 156 throw new BuildException("Missing attribute projectLibrariesRootOut"); 157 } 158 if (mProjectLibrariesResOut == null) { 159 throw new BuildException("Missing attribute projectLibrariesResOut"); 160 } 161 if (mProjectLibrariesPackageOut == null) { 162 throw new BuildException("Missing attribute projectLibrariesPackageOut"); 163 } 164 if (mProjectLibrariesJarsOut == null) { 165 throw new BuildException("Missing attribute projectLibrariesJarsOut"); 166 } 167 if (mProjectLibrariesLibsOut == null) { 168 throw new BuildException("Missing attribute projectLibrariesLibsOut"); 169 } 170 if (mTargetApiOut == null) { 171 throw new BuildException("Missing attribute targetApiOut"); 172 } 173 174 175 Project antProject = getProject(); 176 177 // check the Ant version 178 DeweyDecimal version = getVersion(antProject); 179 DeweyDecimal atLeast = new DeweyDecimal(ANT_MIN_VERSION); 180 if (atLeast.isGreaterThan(version)) { 181 throw new BuildException( 182 "The Android Ant-based build system requires Ant " + 183 ANT_MIN_VERSION + 184 " or later. Current version is " + 185 version); 186 } 187 188 // get the SDK location 189 File sdkDir = TaskHelper.getSdkLocation(antProject); 190 String sdkOsPath = sdkDir.getPath(); 191 192 // Make sure the OS sdk path ends with a directory separator 193 if (sdkOsPath.length() > 0 && !sdkOsPath.endsWith(File.separator)) { 194 sdkOsPath += File.separator; 195 } 196 197 // display SDK Tools revision 198 int toolsRevison = TaskHelper.getToolsRevision(sdkDir); 199 if (toolsRevison != -1) { 200 System.out.println("Android SDK Tools Revision " + toolsRevison); 201 } 202 203 // detect that the platform tools is there. 204 File platformTools = new File(sdkDir, SdkConstants.FD_PLATFORM_TOOLS); 205 if (platformTools.isDirectory() == false) { 206 throw new BuildException(String.format( 207 "SDK Platform Tools component is missing. " + 208 "Please install it with the SDK Manager (%1$s%2$c%3$s)", 209 SdkConstants.FD_TOOLS, 210 File.separatorChar, 211 SdkConstants.androidCmdName())); 212 } 213 214 // get the target property value 215 String targetHashString = antProject.getProperty(ProjectProperties.PROPERTY_TARGET); 216 217 boolean isTestProject = false; 218 219 if (antProject.getProperty(ProjectProperties.PROPERTY_TESTED_PROJECT) != null) { 220 isTestProject = true; 221 } 222 223 if (targetHashString == null) { 224 throw new BuildException("Android Target is not set."); 225 } 226 227 // load up the sdk targets. 228 final ArrayList<String> messages = new ArrayList<String>(); 229 SdkManager manager = SdkManager.createManager(sdkOsPath, new ISdkLog() { 230 public void error(Throwable t, String errorFormat, Object... args) { 231 if (errorFormat != null) { 232 messages.add(String.format("Error: " + errorFormat, args)); 233 } 234 if (t != null) { 235 messages.add("Error: " + t.getMessage()); 236 } 237 } 238 239 public void printf(String msgFormat, Object... args) { 240 messages.add(String.format(msgFormat, args)); 241 } 242 243 public void warning(String warningFormat, Object... args) { 244 messages.add(String.format("Warning: " + warningFormat, args)); 245 } 246 }); 247 248 if (manager == null) { 249 // since we failed to parse the SDK, lets display the parsing output. 250 for (String msg : messages) { 251 System.out.println(msg); 252 } 253 throw new BuildException("Failed to parse SDK content."); 254 } 255 256 // resolve it 257 IAndroidTarget androidTarget = manager.getTargetFromHashString(targetHashString); 258 259 if (androidTarget == null) { 260 throw new BuildException(String.format( 261 "Unable to resolve target '%s'", targetHashString)); 262 } 263 264 // display the project info 265 System.out.println("Project Target: " + androidTarget.getName()); 266 if (androidTarget.isPlatform() == false) { 267 System.out.println("Vendor: " + androidTarget.getVendor()); 268 System.out.println("Platform Version: " + androidTarget.getVersionName()); 269 } 270 System.out.println("API level: " + androidTarget.getVersion().getApiString()); 271 272 // check if the project is a library 273 boolean isLibrary = false; 274 275 String libraryProp = antProject.getProperty(ProjectProperties.PROPERTY_LIBRARY); 276 if (libraryProp != null) { 277 isLibrary = Boolean.valueOf(libraryProp).booleanValue(); 278 } 279 280 if (isLibrary) { 281 System.out.println("Project Type: Android Library"); 282 } 283 284 // look for referenced libraries. 285 processReferencedLibraries(antProject, androidTarget); 286 287 // always check the manifest minSdkVersion. 288 checkManifest(antProject, androidTarget.getVersion()); 289 290 // sets up the properties to find android.jar/framework.aidl/target tools 291 String androidJar = androidTarget.getPath(IAndroidTarget.ANDROID_JAR); 292 antProject.setProperty(mAndroidJarFileOut, androidJar); 293 294 String androidAidl = androidTarget.getPath(IAndroidTarget.ANDROID_AIDL); 295 antProject.setProperty(mAndroidAidlFileOut, androidAidl); 296 297 Path includePath = new Path(antProject); 298 PathElement element = includePath.createPathElement(); 299 element.setPath(androidTarget.getPath(IAndroidTarget.ANDROID_RS)); 300 element = includePath.createPathElement(); 301 element.setPath(androidTarget.getPath(IAndroidTarget.ANDROID_RS_CLANG)); 302 antProject.setProperty(mRenderScriptIncludeDirOut, includePath.toString()); 303 304 // TODO: figure out the actual compiler to use based on the minSdkVersion 305 antProject.setProperty(mRenderScriptExeOut, 306 sdkOsPath + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + 307 SdkConstants.FN_RENDERSCRIPT); 308 309 // sets up the boot classpath 310 311 // create the Path object 312 Path bootclasspath = new Path(antProject); 313 314 // create a PathElement for the framework jar 315 element = bootclasspath.createPathElement(); 316 element.setPath(androidJar); 317 318 // create PathElement for each optional library. 319 IOptionalLibrary[] libraries = androidTarget.getOptionalLibraries(); 320 if (libraries != null) { 321 HashSet<String> visitedJars = new HashSet<String>(); 322 for (IOptionalLibrary library : libraries) { 323 String jarPath = library.getJarPath(); 324 if (visitedJars.contains(jarPath) == false) { 325 visitedJars.add(jarPath); 326 327 element = bootclasspath.createPathElement(); 328 element.setPath(library.getJarPath()); 329 } 330 } 331 } 332 333 // sets the path in the project with a reference 334 antProject.addReference(mBootclasspathrefOut, bootclasspath); 335 336 // finally set the project type. 337 if (isLibrary) { 338 antProject.setProperty(mProjectTypeOut, "library"); 339 } else if (isTestProject) { 340 antProject.setProperty(mProjectTypeOut, "test"); 341 } else { 342 antProject.setProperty(mProjectTypeOut, "project"); 343 } 344 } 345 346 /** 347 * Checks the manifest <code>minSdkVersion</code> attribute. 348 * @param antProject the ant project 349 * @param androidVersion the version of the platform the project is compiling against. 350 */ 351 private void checkManifest(Project antProject, AndroidVersion androidVersion) { 352 try { 353 File manifest = new File(antProject.getBaseDir(), SdkConstants.FN_ANDROID_MANIFEST_XML); 354 355 XPath xPath = AndroidXPathFactory.newXPath(); 356 357 // check the package name. 358 String value = xPath.evaluate( 359 "/" + AndroidManifest.NODE_MANIFEST + 360 "/@" + AndroidManifest.ATTRIBUTE_PACKAGE, 361 new InputSource(new FileInputStream(manifest))); 362 if (value != null) { // aapt will complain if it's missing. 363 // only need to check that the package has 2 segments 364 if (value.indexOf('.') == -1) { 365 throw new BuildException(String.format( 366 "Application package '%1$s' must have a minimum of 2 segments.", 367 value)); 368 } 369 } 370 371 // check the minSdkVersion value 372 value = xPath.evaluate( 373 "/" + AndroidManifest.NODE_MANIFEST + 374 "/" + AndroidManifest.NODE_USES_SDK + 375 "/@" + AndroidXPathFactory.DEFAULT_NS_PREFIX + ":" + 376 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, 377 new InputSource(new FileInputStream(manifest))); 378 379 if (androidVersion.isPreview()) { 380 // in preview mode, the content of the minSdkVersion must match exactly the 381 // platform codename. 382 String codeName = androidVersion.getCodename(); 383 if (codeName.equals(value) == false) { 384 throw new BuildException(String.format( 385 "For '%1$s' SDK Preview, attribute minSdkVersion in AndroidManifest.xml must be '%1$s' (current: %2$s)", 386 codeName, value)); 387 } 388 389 // set the API level to the previous API level (which is actually the value in 390 // androidVersion.) 391 antProject.setProperty(mTargetApiOut, 392 Integer.toString(androidVersion.getApiLevel())); 393 394 } else if (value.length() > 0) { 395 // for normal platform, we'll only display warnings if the value is lower or higher 396 // than the target api level. 397 // First convert to an int. 398 int minSdkValue = -1; 399 try { 400 minSdkValue = Integer.parseInt(value); 401 } catch (NumberFormatException e) { 402 // looks like it's not a number: error! 403 throw new BuildException(String.format( 404 "Attribute %1$s in AndroidManifest.xml must be an Integer!", 405 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION)); 406 } 407 408 // set the target api to the value 409 antProject.setProperty(mTargetApiOut, value); 410 411 int projectApiLevel = androidVersion.getApiLevel(); 412 if (minSdkValue < projectApiLevel) { 413 System.out.println(String.format( 414 "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is lower than the project target API level (%3$d)", 415 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, 416 minSdkValue, projectApiLevel)); 417 } else if (minSdkValue > androidVersion.getApiLevel()) { 418 System.out.println(String.format( 419 "WARNING: Attribute %1$s in AndroidManifest.xml (%2$d) is higher than the project target API level (%3$d)", 420 AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, 421 minSdkValue, projectApiLevel)); 422 } 423 } else { 424 // no minSdkVersion? display a warning 425 System.out.println( 426 "WARNING: No minSdkVersion value set. Application will install on all Android versions."); 427 428 // set the target api to 1 429 antProject.setProperty(mTargetApiOut, "1"); 430 } 431 432 } catch (XPathExpressionException e) { 433 throw new BuildException(e); 434 } catch (FileNotFoundException e) { 435 throw new BuildException(e); 436 } 437 } 438 439 private void processReferencedLibraries(Project antProject, IAndroidTarget androidTarget) { 440 // prepare several paths for future tasks 441 Path rootPath = new Path(antProject); 442 Path resPath = new Path(antProject); 443 Path libsPath = new Path(antProject); 444 Path jarsPath = new Path(antProject); 445 StringBuilder packageStrBuilder = new StringBuilder(); 446 447 FilenameFilter filter = new FilenameFilter() { 448 public boolean accept(File dir, String name) { 449 return name.toLowerCase().endsWith(".jar"); 450 } 451 }; 452 453 System.out.println("\n------------------\nResolving library dependencies:"); 454 455 ArrayList<File> libraries = getProjectLibraries(antProject); 456 457 if (libraries.size() > 0) { 458 System.out.println("------------------\nOrdered libraries:"); 459 460 for (File library : libraries) { 461 String libRootPath = library.getAbsolutePath(); 462 System.out.println(libRootPath); 463 464 // get the root path. 465 PathElement element = rootPath.createPathElement(); 466 element.setPath(libRootPath); 467 468 // get the res path. Always $PROJECT/res as well as the crunch cache. 469 element = resPath.createPathElement(); 470 element.setPath(libRootPath + "/" + SdkConstants.FD_OUTPUT + 471 "/" + SdkConstants.FD_RES); 472 element = resPath.createPathElement(); 473 element.setPath(libRootPath + "/" + SdkConstants.FD_RESOURCES); 474 475 // get the libs path. Always $PROJECT/libs 476 element = libsPath.createPathElement(); 477 element.setPath(libRootPath + "/" + SdkConstants.FD_NATIVE_LIBS); 478 479 // get the jars from it too. 480 // 1. the library code jar 481 element = jarsPath.createPathElement(); 482 element.setPath(libRootPath + "/" + SdkConstants.FD_OUTPUT + 483 "/" + SdkConstants.FN_CLASSES_JAR); 484 485 // 2. the 3rd party jar files 486 File libsFolder = new File(library, SdkConstants.FD_NATIVE_LIBS); 487 File[] jarFiles = libsFolder.listFiles(filter); 488 if (jarFiles != null) { 489 for (File jarFile : jarFiles) { 490 element = jarsPath.createPathElement(); 491 element.setPath(jarFile.getAbsolutePath()); 492 } 493 } 494 495 // get the package from the manifest. 496 FileWrapper manifest = new FileWrapper(library, 497 SdkConstants.FN_ANDROID_MANIFEST_XML); 498 499 try { 500 String value = AndroidManifest.getPackage(manifest); 501 if (value != null) { // aapt will complain if it's missing. 502 packageStrBuilder.append(';'); 503 packageStrBuilder.append(value); 504 } 505 } catch (Exception e) { 506 throw new BuildException(e); 507 } 508 } 509 } else { 510 System.out.println("No library dependencies.\n"); 511 } 512 513 System.out.println("------------------\n"); 514 515 // even with no libraries, always setup these so that various tasks in Ant don't complain 516 // (the task themselves can handle a ref to an empty Path) 517 antProject.addReference(mProjectLibrariesJarsOut, jarsPath); 518 antProject.addReference(mProjectLibrariesLibsOut, libsPath); 519 520 // the rest is done only if there's a library. 521 if (jarsPath.list().length > 0) { 522 antProject.addReference(mProjectLibrariesRootOut, rootPath); 523 antProject.addReference(mProjectLibrariesResOut, resPath); 524 antProject.setProperty(mProjectLibrariesPackageOut, packageStrBuilder.toString()); 525 } 526 } 527 528 /** 529 * Returns all the library dependencies of a given Ant project. 530 * @param antProject the Ant project 531 * @return a list of properties, sorted from highest priority to lowest. 532 */ 533 private ArrayList<File> getProjectLibraries(final Project antProject) { 534 ArrayList<File> libraries = new ArrayList<File>(); 535 File baseDir = antProject.getBaseDir(); 536 537 // get the top level list of library dependencies. 538 List<File> topLevelLibraries = getDirectDependencies(baseDir, new IPropertySource() { 539 public String getProperty(String name) { 540 return antProject.getProperty(name); 541 } 542 }); 543 544 // process the libraries in case they depend on other libraries. 545 resolveFullLibraryDependencies(topLevelLibraries, libraries); 546 547 return libraries; 548 } 549 550 /** 551 * Resolves a given list of libraries, finds out if they depend on other libraries, and 552 * returns a full list of all the direct and indirect dependencies in the proper order (first 553 * is higher priority when calling aapt). 554 * @param inLibraries the libraries to resolve 555 * @param outLibraries where to store all the libraries. 556 */ 557 private void resolveFullLibraryDependencies(List<File> inLibraries, List<File> outLibraries) { 558 // loop in the inverse order to resolve dependencies on the libraries, so that if a library 559 // is required by two higher level libraries it can be inserted in the correct place 560 for (int i = inLibraries.size() - 1 ; i >= 0 ; i--) { 561 File library = inLibraries.get(i); 562 563 // get the default.property file for it 564 final ProjectProperties projectProp = ProjectProperties.load( 565 new FolderWrapper(library), PropertyType.PROJECT); 566 567 // get its libraries 568 List<File> dependencies = getDirectDependencies(library, new IPropertySource() { 569 public String getProperty(String name) { 570 return projectProp.getProperty(name); 571 } 572 }); 573 574 // resolve the dependencies for those libraries 575 resolveFullLibraryDependencies(dependencies, outLibraries); 576 577 // and add the current one (if needed) in front (higher priority) 578 if (outLibraries.contains(library) == false) { 579 outLibraries.add(0, library); 580 } 581 } 582 } 583 584 public interface IPropertySource { 585 String getProperty(String name); 586 } 587 588 /** 589 * Returns the top level library dependencies of a given <var>source</var> representing a 590 * project properties. 591 * @param baseFolder the base folder of the project (to resolve relative paths) 592 * @param source a source of project properties. 593 */ 594 private List<File> getDirectDependencies(File baseFolder, IPropertySource source) { 595 ArrayList<File> libraries = new ArrayList<File>(); 596 597 // first build the list. they are ordered highest priority first. 598 int index = 1; 599 while (true) { 600 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index++); 601 String rootPath = source.getProperty(propName); 602 603 if (rootPath == null) { 604 break; 605 } 606 607 try { 608 File library = new File(baseFolder, rootPath).getCanonicalFile(); 609 610 // check for validity 611 File projectProp = new File(library, PropertyType.PROJECT.getFilename()); 612 if (projectProp.isFile() == false) { 613 // error! 614 throw new BuildException(String.format( 615 "%1$s resolve to a path with no %2$s file for project %3$s", rootPath, 616 PropertyType.PROJECT.getFilename(), baseFolder.getAbsolutePath())); 617 } 618 619 if (libraries.contains(library) == false) { 620 System.out.println(String.format("%1$s: %2$s => %3$s", 621 baseFolder.getAbsolutePath(), rootPath, library.getAbsolutePath())); 622 623 libraries.add(library); 624 } 625 } catch (IOException e) { 626 throw new BuildException("Failed to resolve library path: " + rootPath, e); 627 } 628 } 629 630 return libraries; 631 } 632 633 /** 634 * Returns the Ant version as a {@link DeweyDecimal} object. 635 * 636 * This is based on the implementation of 637 * org.apache.tools.ant.taskdefs.condition.AntVersion.getVersion() 638 * 639 * @param antProject the current ant project. 640 * @return the ant version. 641 */ 642 private DeweyDecimal getVersion(Project antProject) { 643 char[] versionString = antProject.getProperty("ant.version").toCharArray(); 644 StringBuilder sb = new StringBuilder(); 645 boolean foundFirstDigit = false; 646 for (int i = 0; i < versionString.length; i++) { 647 if (Character.isDigit(versionString[i])) { 648 sb.append(versionString[i]); 649 foundFirstDigit = true; 650 } 651 if (versionString[i] == '.' && foundFirstDigit) { 652 sb.append(versionString[i]); 653 } 654 if (Character.isLetter(versionString[i]) && foundFirstDigit) { 655 break; 656 } 657 } 658 return new DeweyDecimal(sb.toString()); 659 } 660 } 661