1 /* 2 * Copyright (C) 2011 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.project; 18 19 import static com.android.ide.eclipse.adt.AdtConstants.CONTAINER_DEPENDENCIES; 20 21 import com.android.SdkConstants; 22 import com.android.ide.common.sdk.LoadStatus; 23 import com.android.ide.eclipse.adt.AdtConstants; 24 import com.android.ide.eclipse.adt.AdtPlugin; 25 import com.android.ide.eclipse.adt.AndroidPrintStream; 26 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 27 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 28 import com.android.sdklib.BuildToolInfo; 29 import com.android.sdklib.build.JarListSanitizer; 30 import com.android.sdklib.build.JarListSanitizer.DifferentLibException; 31 import com.android.sdklib.build.JarListSanitizer.Sha1Exception; 32 import com.android.sdklib.build.RenderScriptProcessor; 33 34 import org.eclipse.core.resources.IFile; 35 import org.eclipse.core.resources.IFolder; 36 import org.eclipse.core.resources.IProject; 37 import org.eclipse.core.resources.IResource; 38 import org.eclipse.core.resources.IWorkspaceRoot; 39 import org.eclipse.core.resources.ResourcesPlugin; 40 import org.eclipse.core.runtime.CoreException; 41 import org.eclipse.core.runtime.IPath; 42 import org.eclipse.core.runtime.NullProgressMonitor; 43 import org.eclipse.core.runtime.Path; 44 import org.eclipse.jdt.core.IAccessRule; 45 import org.eclipse.jdt.core.IClasspathAttribute; 46 import org.eclipse.jdt.core.IClasspathContainer; 47 import org.eclipse.jdt.core.IClasspathEntry; 48 import org.eclipse.jdt.core.IJavaProject; 49 import org.eclipse.jdt.core.JavaCore; 50 import org.eclipse.jdt.core.JavaModelException; 51 52 import java.io.File; 53 import java.io.FileInputStream; 54 import java.io.FileNotFoundException; 55 import java.io.IOException; 56 import java.io.InputStream; 57 import java.net.MalformedURLException; 58 import java.util.ArrayList; 59 import java.util.HashSet; 60 import java.util.List; 61 import java.util.Properties; 62 import java.util.Set; 63 64 public class LibraryClasspathContainerInitializer extends BaseClasspathContainerInitializer { 65 66 private final static String ATTR_SRC = "src"; //$NON-NLS-1$ 67 private final static String ATTR_DOC = "doc"; //$NON-NLS-1$ 68 private final static String DOT_PROPERTIES = ".properties"; //$NON-NLS-1$ 69 70 public LibraryClasspathContainerInitializer() { 71 } 72 73 /** 74 * Updates the {@link IJavaProject} objects with new library. 75 * @param androidProjects the projects to update. 76 * @return <code>true</code> if success, <code>false</code> otherwise. 77 */ 78 public static boolean updateProjects(IJavaProject[] androidProjects) { 79 try { 80 // Allocate a new AndroidClasspathContainer, and associate it to the library 81 // container id for each projects. 82 int projectCount = androidProjects.length; 83 84 IClasspathContainer[] libraryContainers = new IClasspathContainer[projectCount]; 85 IClasspathContainer[] dependencyContainers = new IClasspathContainer[projectCount]; 86 for (int i = 0 ; i < projectCount; i++) { 87 libraryContainers[i] = allocateLibraryContainer(androidProjects[i]); 88 dependencyContainers[i] = allocateDependencyContainer(androidProjects[i]); 89 } 90 91 // give each project their new container in one call. 92 JavaCore.setClasspathContainer( 93 new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 94 androidProjects, libraryContainers, new NullProgressMonitor()); 95 96 JavaCore.setClasspathContainer( 97 new Path(AdtConstants.CONTAINER_DEPENDENCIES), 98 androidProjects, dependencyContainers, new NullProgressMonitor()); 99 return true; 100 } catch (JavaModelException e) { 101 return false; 102 } 103 } 104 105 /** 106 * Updates the {@link IJavaProject} objects with new library. 107 * @param androidProjects the projects to update. 108 * @return <code>true</code> if success, <code>false</code> otherwise. 109 */ 110 public static boolean updateProject(List<ProjectState> projects) { 111 List<IJavaProject> javaProjectList = new ArrayList<IJavaProject>(projects.size()); 112 for (ProjectState p : projects) { 113 IJavaProject javaProject = JavaCore.create(p.getProject()); 114 if (javaProject != null) { 115 javaProjectList.add(javaProject); 116 } 117 } 118 119 IJavaProject[] javaProjects = javaProjectList.toArray( 120 new IJavaProject[javaProjectList.size()]); 121 122 return updateProjects(javaProjects); 123 } 124 125 @Override 126 public void initialize(IPath containerPath, IJavaProject project) throws CoreException { 127 if (AdtConstants.CONTAINER_PRIVATE_LIBRARIES.equals(containerPath.toString())) { 128 IClasspathContainer libraries = allocateLibraryContainer(project); 129 if (libraries != null) { 130 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 131 new IJavaProject[] { project }, 132 new IClasspathContainer[] { libraries }, 133 new NullProgressMonitor()); 134 } 135 136 } else if(AdtConstants.CONTAINER_DEPENDENCIES.equals(containerPath.toString())) { 137 IClasspathContainer dependencies = allocateDependencyContainer(project); 138 if (dependencies != null) { 139 JavaCore.setClasspathContainer(new Path(AdtConstants.CONTAINER_DEPENDENCIES), 140 new IJavaProject[] { project }, 141 new IClasspathContainer[] { dependencies }, 142 new NullProgressMonitor()); 143 } 144 } 145 } 146 147 private static IClasspathContainer allocateLibraryContainer(IJavaProject javaProject) { 148 final IProject iProject = javaProject.getProject(); 149 150 // check if the project has a valid target. 151 ProjectState state = Sdk.getProjectState(iProject); 152 if (state == null) { 153 // getProjectState should already have logged an error. Just bail out. 154 return null; 155 } 156 157 /* 158 * At this point we're going to gather a list of all that need to go in the 159 * dependency container. 160 * - Library project outputs (direct and indirect) 161 * - Java project output (those can be indirectly referenced through library projects 162 * or other other Java projects) 163 * - Jar files: 164 * + inside this project's libs/ 165 * + inside the library projects' libs/ 166 * + inside the referenced Java projects' classpath 167 */ 168 List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); 169 170 // list of java project dependencies and jar files that will be built while 171 // going through the library projects. 172 Set<File> jarFiles = new HashSet<File>(); 173 Set<IProject> refProjects = new HashSet<IProject>(); 174 175 // process all the libraries 176 177 List<IProject> libProjects = state.getFullLibraryProjects(); 178 for (IProject libProject : libProjects) { 179 // process all of the library project's dependencies 180 getDependencyListFromClasspath(libProject, refProjects, jarFiles, true); 181 } 182 183 // now process this projects' referenced projects only. 184 processReferencedProjects(iProject, refProjects, jarFiles); 185 186 // and the content of its libs folder 187 getJarListFromLibsFolder(iProject, jarFiles); 188 189 // now add a classpath entry for each Java project (this is a set so dups are already 190 // removed) 191 for (IProject p : refProjects) { 192 entries.add(JavaCore.newProjectEntry(p.getFullPath(), true /*isExported*/)); 193 } 194 195 entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); 196 197 return allocateContainer(javaProject, entries, new Path(AdtConstants.CONTAINER_PRIVATE_LIBRARIES), 198 "Android Private Libraries"); 199 } 200 201 private static List<IClasspathEntry> convertJarsToClasspathEntries(final IProject iProject, 202 Set<File> jarFiles) { 203 List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(jarFiles.size()); 204 205 // and process the jar files list, but first sanitize it to remove dups. 206 JarListSanitizer sanitizer = new JarListSanitizer( 207 iProject.getFolder(SdkConstants.FD_OUTPUT).getLocation().toFile(), 208 new AndroidPrintStream(iProject, null /*prefix*/, 209 AdtPlugin.getOutStream())); 210 211 String errorMessage = null; 212 213 try { 214 List<File> sanitizedList = sanitizer.sanitize(jarFiles); 215 216 for (File jarFile : sanitizedList) { 217 if (jarFile instanceof CPEFile) { 218 CPEFile cpeFile = (CPEFile) jarFile; 219 IClasspathEntry e = cpeFile.getClasspathEntry(); 220 221 entries.add(JavaCore.newLibraryEntry( 222 e.getPath(), 223 e.getSourceAttachmentPath(), 224 e.getSourceAttachmentRootPath(), 225 e.getAccessRules(), 226 e.getExtraAttributes(), 227 true /*isExported*/)); 228 } else { 229 String jarPath = jarFile.getAbsolutePath(); 230 231 IPath sourceAttachmentPath = null; 232 IClasspathAttribute javaDocAttribute = null; 233 234 File jarProperties = new File(jarPath + DOT_PROPERTIES); 235 if (jarProperties.isFile()) { 236 Properties p = new Properties(); 237 InputStream is = null; 238 try { 239 p.load(is = new FileInputStream(jarProperties)); 240 241 String value = p.getProperty(ATTR_SRC); 242 if (value != null) { 243 File srcPath = getFile(jarFile, value); 244 245 if (srcPath.exists()) { 246 sourceAttachmentPath = new Path(srcPath.getAbsolutePath()); 247 } 248 } 249 250 value = p.getProperty(ATTR_DOC); 251 if (value != null) { 252 File docPath = getFile(jarFile, value); 253 if (docPath.exists()) { 254 try { 255 javaDocAttribute = JavaCore.newClasspathAttribute( 256 IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, 257 docPath.toURI().toURL().toString()); 258 } catch (MalformedURLException e) { 259 AdtPlugin.log(e, "Failed to process 'doc' attribute for %s", 260 jarProperties.getAbsolutePath()); 261 } 262 } 263 } 264 265 } catch (FileNotFoundException e) { 266 // shouldn't happen since we check upfront 267 } catch (IOException e) { 268 AdtPlugin.log(e, "Failed to read %s", jarProperties.getAbsolutePath()); 269 } finally { 270 if (is != null) { 271 try { 272 is.close(); 273 } catch (IOException e) { 274 // ignore 275 } 276 } 277 } 278 } 279 280 if (javaDocAttribute != null) { 281 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 282 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 283 new IAccessRule[0], 284 new IClasspathAttribute[] { javaDocAttribute }, 285 true /*isExported*/)); 286 } else { 287 entries.add(JavaCore.newLibraryEntry(new Path(jarPath), 288 sourceAttachmentPath, null /*sourceAttachmentRootPath*/, 289 true /*isExported*/)); 290 } 291 } 292 } 293 } catch (DifferentLibException e) { 294 errorMessage = e.getMessage(); 295 AdtPlugin.printErrorToConsole(iProject, (Object[]) e.getDetails()); 296 } catch (Sha1Exception e) { 297 errorMessage = e.getMessage(); 298 } 299 300 processError(iProject, errorMessage, AdtConstants.MARKER_DEPENDENCY, 301 true /*outputToConsole*/); 302 303 return entries; 304 } 305 306 private static IClasspathContainer allocateDependencyContainer(IJavaProject javaProject) { 307 final IProject iProject = javaProject.getProject(); 308 final List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); 309 final Set<File> jarFiles = new HashSet<File>(); 310 final IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); 311 312 AdtPlugin plugin = AdtPlugin.getDefault(); 313 if (plugin == null) { // This is totally weird, but I've seen it happen! 314 return null; 315 } 316 317 synchronized (Sdk.getLock()) { 318 boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED; 319 320 // check if the project has a valid target. 321 final ProjectState state = Sdk.getProjectState(iProject); 322 if (state == null) { 323 // getProjectState should already have logged an error. Just bail out. 324 return null; 325 } 326 327 // annotations support for older version of android 328 if (state.getTarget() != null && state.getTarget().getVersion().getApiLevel() <= 15) { 329 File annotationsJar = new File(Sdk.getCurrent().getSdkOsLocation(), 330 SdkConstants.FD_TOOLS + File.separator + SdkConstants.FD_SUPPORT + 331 File.separator + SdkConstants.FN_ANNOTATIONS_JAR); 332 333 jarFiles.add(annotationsJar); 334 } 335 336 if (state.getRenderScriptSupportMode()) { 337 if (!sdkIsLoaded) { 338 return null; 339 } 340 BuildToolInfo buildToolInfo = state.getBuildToolInfo(); 341 if (buildToolInfo == null) { 342 buildToolInfo = Sdk.getCurrent().getLatestBuildTool(); 343 344 if (buildToolInfo == null) { 345 return null; 346 } 347 } 348 349 File renderScriptSupportJar = RenderScriptProcessor.getSupportJar( 350 buildToolInfo.getLocation().getAbsolutePath()); 351 352 jarFiles.add(renderScriptSupportJar); 353 } 354 355 // process all the libraries 356 357 List<IProject> libProjects = state.getFullLibraryProjects(); 358 for (IProject libProject : libProjects) { 359 // get the project output 360 IFolder outputFolder = BaseProjectHelper.getAndroidOutputFolder(libProject); 361 362 if (outputFolder != null) { // can happen when closing/deleting a library) 363 IFile jarIFile = outputFolder.getFile(libProject.getName().toLowerCase() + 364 SdkConstants.DOT_JAR); 365 366 // get the source folder for the library project 367 List<IPath> srcs = BaseProjectHelper.getSourceClasspaths(libProject); 368 // find the first non-derived source folder. 369 IPath sourceFolder = null; 370 for (IPath src : srcs) { 371 IFolder srcFolder = workspaceRoot.getFolder(src); 372 if (srcFolder.isDerived() == false) { 373 sourceFolder = src; 374 break; 375 } 376 } 377 378 // we can directly add a CPE for this jar as there's no risk of a duplicate. 379 IClasspathEntry entry = JavaCore.newLibraryEntry( 380 jarIFile.getLocation(), 381 sourceFolder, // source attachment path 382 null, // default source attachment root path. 383 true /*isExported*/); 384 385 entries.add(entry); 386 } 387 } 388 389 entries.addAll(convertJarsToClasspathEntries(iProject, jarFiles)); 390 391 return allocateContainer(javaProject, entries, new Path(CONTAINER_DEPENDENCIES), 392 "Android Dependencies"); 393 } 394 } 395 396 private static IClasspathContainer allocateContainer(IJavaProject javaProject, 397 List<IClasspathEntry> entries, IPath id, String description) { 398 399 if (AdtPlugin.getDefault() == null) { // This is totally weird, but I've seen it happen! 400 return null; 401 } 402 403 // First check that the project has a library-type container. 404 try { 405 IClasspathEntry[] rawClasspath = javaProject.getRawClasspath(); 406 final IClasspathEntry[] oldRawClasspath = rawClasspath; 407 408 boolean foundContainer = false; 409 for (IClasspathEntry entry : rawClasspath) { 410 // get the entry and kind 411 final int kind = entry.getEntryKind(); 412 413 if (kind == IClasspathEntry.CPE_CONTAINER) { 414 String path = entry.getPath().toString(); 415 String idString = id.toString(); 416 if (idString.equals(path)) { 417 foundContainer = true; 418 break; 419 } 420 } 421 } 422 423 // if there isn't any, add it. 424 if (foundContainer == false) { 425 // add the android container to the array 426 rawClasspath = ProjectHelper.addEntryToClasspath(rawClasspath, 427 JavaCore.newContainerEntry(id, true /*isExported*/)); 428 } 429 430 // set the new list of entries to the project 431 if (rawClasspath != oldRawClasspath) { 432 javaProject.setRawClasspath(rawClasspath, new NullProgressMonitor()); 433 } 434 } catch (JavaModelException e) { 435 // This really shouldn't happen, but if it does, simply return null (the calling 436 // method will fails as well) 437 return null; 438 } 439 440 return new AndroidClasspathContainer( 441 entries.toArray(new IClasspathEntry[entries.size()]), 442 id, 443 description, 444 IClasspathContainer.K_APPLICATION); 445 } 446 447 private static File getFile(File root, String value) { 448 File file = new File(value); 449 if (file.isAbsolute() == false) { 450 file = new File(root.getParentFile(), value); 451 } 452 453 return file; 454 } 455 456 /** 457 * Finds all the jar files inside a project's libs folder. 458 * @param project 459 * @param jarFiles 460 */ 461 private static void getJarListFromLibsFolder(IProject project, Set<File> jarFiles) { 462 IFolder libsFolder = project.getFolder(SdkConstants.FD_NATIVE_LIBS); 463 if (libsFolder.exists()) { 464 try { 465 IResource[] members = libsFolder.members(); 466 for (IResource member : members) { 467 if (member.getType() == IResource.FILE && 468 SdkConstants.EXT_JAR.equalsIgnoreCase(member.getFileExtension())) { 469 IPath location = member.getLocation(); 470 if (location != null) { 471 jarFiles.add(location.toFile()); 472 } 473 } 474 } 475 } catch (CoreException e) { 476 // can't get the list? ignore this folder. 477 } 478 } 479 } 480 481 /** 482 * Process reference projects from the main projects to add indirect dependencies coming 483 * from Java project. 484 * @param project the main project 485 * @param projects the project list to add to 486 * @param jarFiles the jar list to add to. 487 */ 488 private static void processReferencedProjects(IProject project, 489 Set<IProject> projects, Set<File> jarFiles) { 490 try { 491 IProject[] refs = project.getReferencedProjects(); 492 for (IProject p : refs) { 493 // ignore if it's an Android project, or if it's not a Java 494 // Project 495 if (p.hasNature(JavaCore.NATURE_ID) 496 && p.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 497 498 // process this project's dependencies 499 getDependencyListFromClasspath(p, projects, jarFiles, true /*includeJarFiles*/); 500 } 501 } 502 } catch (CoreException e) { 503 // can't get the referenced projects? ignore 504 } 505 } 506 507 /** 508 * Finds all the dependencies of a given project and add them to a project list and 509 * a jar list. 510 * Only classpath entries that are exported are added, and only Java project (not Android 511 * project) are added. 512 * 513 * @param project the project to query 514 * @param projects the referenced project list to add to 515 * @param jarFiles the jar list to add to 516 * @param includeJarFiles whether to include jar files or just projects. This is useful when 517 * calling on an Android project (value should be <code>false</code>) 518 */ 519 private static void getDependencyListFromClasspath(IProject project, Set<IProject> projects, 520 Set<File> jarFiles, boolean includeJarFiles) { 521 IJavaProject javaProject = JavaCore.create(project); 522 IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); 523 524 // we could use IJavaProject.getResolvedClasspath directly, but we actually 525 // want to see the containers themselves. 526 IClasspathEntry[] classpaths = javaProject.readRawClasspath(); 527 if (classpaths != null) { 528 for (IClasspathEntry e : classpaths) { 529 // ignore entries that are not exported 530 if (!e.getPath().toString().equals(CONTAINER_DEPENDENCIES) && e.isExported()) { 531 processCPE(e, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 532 } 533 } 534 } 535 } 536 537 /** 538 * Processes a {@link IClasspathEntry} and add it to one of the list if applicable. 539 * @param entry the entry to process 540 * @param javaProject the {@link IJavaProject} from which this entry came. 541 * @param wsRoot the {@link IWorkspaceRoot} 542 * @param projects the project list to add to 543 * @param jarFiles the jar list to add to 544 * @param includeJarFiles whether to include jar files or just projects. This is useful when 545 * calling on an Android project (value should be <code>false</code>) 546 */ 547 private static void processCPE(IClasspathEntry entry, IJavaProject javaProject, 548 IWorkspaceRoot wsRoot, 549 Set<IProject> projects, Set<File> jarFiles, boolean includeJarFiles) { 550 551 // if this is a classpath variable reference, we resolve it. 552 if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) { 553 entry = JavaCore.getResolvedClasspathEntry(entry); 554 } 555 556 if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { 557 IProject refProject = wsRoot.getProject(entry.getPath().lastSegment()); 558 try { 559 // ignore if it's an Android project, or if it's not a Java Project 560 if (refProject.hasNature(JavaCore.NATURE_ID) && 561 refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) { 562 // add this project to the list 563 projects.add(refProject); 564 565 // also get the dependency from this project. 566 getDependencyListFromClasspath(refProject, projects, jarFiles, 567 true /*includeJarFiles*/); 568 } 569 } catch (CoreException exception) { 570 // can't query the project nature? ignore 571 } 572 } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { 573 if (includeJarFiles) { 574 handleClasspathLibrary(entry, wsRoot, jarFiles); 575 } 576 } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) { 577 // get the container and its content 578 try { 579 IClasspathContainer container = JavaCore.getClasspathContainer( 580 entry.getPath(), javaProject); 581 // ignore the system and default_system types as they represent 582 // libraries that are part of the runtime. 583 if (container != null && 584 container.getKind() == IClasspathContainer.K_APPLICATION) { 585 IClasspathEntry[] entries = container.getClasspathEntries(); 586 for (IClasspathEntry cpe : entries) { 587 processCPE(cpe, javaProject, wsRoot, projects, jarFiles, includeJarFiles); 588 } 589 } 590 } catch (JavaModelException jme) { 591 // can't resolve the container? ignore it. 592 AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath()); 593 } 594 } 595 } 596 597 private static final class CPEFile extends File { 598 private static final long serialVersionUID = 1L; 599 600 private final IClasspathEntry mClasspathEntry; 601 602 public CPEFile(String pathname, IClasspathEntry classpathEntry) { 603 super(pathname); 604 mClasspathEntry = classpathEntry; 605 } 606 607 public CPEFile(File file, IClasspathEntry classpathEntry) { 608 super(file.getAbsolutePath()); 609 mClasspathEntry = classpathEntry; 610 } 611 612 public IClasspathEntry getClasspathEntry() { 613 return mClasspathEntry; 614 } 615 } 616 617 private static void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot, 618 Set<File> jarFiles) { 619 // get the IPath 620 IPath path = e.getPath(); 621 622 IResource resource = wsRoot.findMember(path); 623 624 if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) { 625 // case of a jar file (which could be relative to the workspace or a full path) 626 if (resource != null && resource.exists() && 627 resource.getType() == IResource.FILE) { 628 jarFiles.add(new CPEFile(resource.getLocation().toFile(), e)); 629 } else { 630 // if the jar path doesn't match a workspace resource, 631 // then we get an OSString and check if this links to a valid file. 632 String osFullPath = path.toOSString(); 633 634 File f = new CPEFile(osFullPath, e); 635 if (f.isFile()) { 636 jarFiles.add(f); 637 } 638 } 639 } 640 } 641 } 642