1 /******************************************************************************* 2 * Copyright (c) 2000, 2019 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.jdt.internal.core; 15 16 import java.io.IOException; 17 import java.io.InputStream; 18 import java.net.URL; 19 import java.util.ArrayList; 20 import java.util.Arrays; 21 import java.util.Enumeration; 22 import java.util.HashMap; 23 import java.util.List; 24 import java.util.Map; 25 import java.util.jar.Manifest; 26 import java.util.zip.ZipEntry; 27 import java.util.zip.ZipException; 28 import java.util.zip.ZipFile; 29 30 import org.eclipse.core.resources.IResource; 31 import org.eclipse.core.runtime.CoreException; 32 import org.eclipse.core.runtime.IPath; 33 import org.eclipse.core.runtime.IStatus; 34 import org.eclipse.jdt.core.IClasspathAttribute; 35 import org.eclipse.jdt.core.IClasspathEntry; 36 import org.eclipse.jdt.core.IJavaElement; 37 import org.eclipse.jdt.core.IModuleDescription; 38 import org.eclipse.jdt.core.IPackageFragmentRoot; 39 import org.eclipse.jdt.core.JavaCore; 40 import org.eclipse.jdt.core.JavaModelException; 41 import org.eclipse.jdt.core.compiler.CharOperation; 42 import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; 43 import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; 44 import org.eclipse.jdt.internal.compiler.lookup.TypeConstants; 45 import org.eclipse.jdt.internal.core.JavaModelManager.PerProjectInfo; 46 import org.eclipse.jdt.internal.core.nd.IReader; 47 import org.eclipse.jdt.internal.core.nd.java.JavaIndex; 48 import org.eclipse.jdt.internal.core.nd.java.NdResourceFile; 49 import org.eclipse.jdt.internal.core.nd.java.NdType; 50 import org.eclipse.jdt.internal.core.nd.java.NdZipEntry; 51 import org.eclipse.jdt.internal.core.util.HashtableOfArrayToObject; 52 import org.eclipse.jdt.internal.core.util.Util; 53 54 /** 55 * A package fragment root that corresponds to a .jar or .zip. 56 * 57 * <p>NOTE: The only visible entries from a .jar or .zip package fragment root 58 * are .class files. 59 * <p>NOTE: A jar package fragment root may or may not have an associated resource. 60 * 61 * @see org.eclipse.jdt.core.IPackageFragmentRoot 62 * @see org.eclipse.jdt.internal.core.JarPackageFragmentRootInfo 63 */ 64 @SuppressWarnings({"rawtypes", "unchecked"}) 65 public class JarPackageFragmentRoot extends PackageFragmentRoot { 66 67 protected final static ArrayList EMPTY_LIST = new ArrayList(); 68 69 /** 70 * The path to the jar file 71 * (a workspace relative path if the jar is internal, 72 * or an OS path if the jar is external) 73 */ 74 protected final IPath jarPath; 75 76 boolean knownToBeModuleLess; 77 78 private boolean multiVersion; 79 80 /** 81 * Reflects the extra attributes of the classpath entry declaring this root. 82 * Caution, this field is used in hashCode() & equals() to avoid overzealous sharing. 83 * Can be null, if lookup via the corresponding classpath entry failed. 84 */ 85 final protected IClasspathAttribute[] extraAttributes; 86 87 /** 88 * Constructs a package fragment root which is the root of the Java package directory hierarchy 89 * based on a JAR file. 90 */ JarPackageFragmentRoot(IResource resource, IPath externalJarPath, JavaProject project, IClasspathAttribute[] attributes)91 public JarPackageFragmentRoot(IResource resource, IPath externalJarPath, JavaProject project, IClasspathAttribute[] attributes) { 92 super(resource, project); 93 this.jarPath = externalJarPath; 94 if (attributes == null) { 95 // attributes could either be 96 // (1) provided by the caller (particularly when creating from memento), 97 // (2) retrieved from the corresponding classpath entry (if a resolved classpath is available). 98 // These two cases should cover all normal scenarios, else extraAttributes will be null. 99 try { 100 PerProjectInfo perProjectInfo = project.getPerProjectInfo(); 101 synchronized (perProjectInfo) { 102 if (perProjectInfo.resolvedClasspath != null && perProjectInfo.unresolvedEntryStatus == JavaModelStatus.VERIFIED_OK) { 103 IClasspathEntry classpathEntry = project.getClasspathEntryFor(externalJarPath); 104 if (classpathEntry != null) 105 attributes = classpathEntry.getExtraAttributes(); 106 } 107 } 108 } catch (JavaModelException e) { 109 // ignore 110 } 111 } 112 this.extraAttributes = attributes; 113 } 114 115 /** 116 * Compute the package fragment children of this package fragment root. 117 * These are all of the directory zip entries, and any directories implied 118 * by the path of class files contained in the jar of this package fragment root. 119 */ 120 @Override computeChildren(OpenableElementInfo info, IResource underlyingResource)121 protected boolean computeChildren(OpenableElementInfo info, IResource underlyingResource) throws JavaModelException { 122 final HashtableOfArrayToObject rawPackageInfo = new HashtableOfArrayToObject(); 123 final Map<String, String> overridden = new HashMap<>(); 124 IJavaElement[] children = NO_ELEMENTS; 125 try { 126 // always create the default package 127 rawPackageInfo.put(CharOperation.NO_STRINGS, new ArrayList[] { EMPTY_LIST, EMPTY_LIST }); 128 129 boolean usedIndex = false; 130 if (JavaIndex.isEnabled()) { 131 JavaIndex index = JavaIndex.getIndex(); 132 try (IReader reader = index.getNd().acquireReadLock()) { 133 IPath resourcePath = JavaIndex.getLocationForElement(this); 134 if (!resourcePath.isEmpty()) { 135 NdResourceFile resourceFile = index.getResourceFile(resourcePath.toString().toCharArray()); 136 if (index.isUpToDate(resourceFile)) { 137 usedIndex = true; 138 long level = resourceFile.getJdkLevel(); 139 String compliance = CompilerOptions.versionFromJdkLevel(level); 140 // Locate all the non-classfile entries 141 for (NdZipEntry next : resourceFile.getZipEntries()) { 142 String filename = next.getFileName().getString(); 143 initRawPackageInfo(rawPackageInfo, filename, filename.endsWith("/"), compliance); //$NON-NLS-1$ 144 } 145 146 // Locate all the classfile entries 147 for (NdType type : resourceFile.getTypes()) { 148 String path = new String(type.getTypeId().getBinaryName()) + ".class"; //$NON-NLS-1$ 149 initRawPackageInfo(rawPackageInfo, path, false, compliance); 150 } 151 } 152 } 153 } 154 } 155 156 // If we weren't able to compute the set of children from the index (either the index was disabled or didn't 157 // contain an up-to-date entry for this .jar) then fetch it directly from the .jar 158 if (!usedIndex) { 159 Object file = JavaModel.getTarget(getPath(), true); 160 long classLevel = Util.getJdkLevel(file); 161 String projectCompliance = this.getJavaProject().getOption(JavaCore.COMPILER_COMPLIANCE, true); 162 long projectLevel = CompilerOptions.versionToJdkLevel(projectCompliance); 163 ZipFile jar = null; 164 try { 165 jar = getJar(); 166 String version = "META-INF/versions/"; //$NON-NLS-1$ 167 List<String> versions = new ArrayList<>(); 168 if (projectLevel >= ClassFileConstants.JDK9 && jar.getEntry(version) != null) { 169 int earliestJavaVersion = ClassFileConstants.MAJOR_VERSION_9; 170 long latestJDK = CompilerOptions.releaseToJDKLevel(projectCompliance); 171 int latestJavaVer = (int) (latestJDK >> 16); 172 173 for(int i = latestJavaVer; i >= earliestJavaVersion; i--) { 174 String s = "" + + (i - 44); //$NON-NLS-1$ 175 String versionPath = version + s; 176 if (jar.getEntry(versionPath) != null) { 177 versions.add(s); 178 } 179 } 180 } 181 182 String[] supportedVersions = versions.toArray(new String[versions.size()]); 183 if (supportedVersions.length > 0) { 184 this.multiVersion = true; 185 } 186 int length = version.length(); 187 for (Enumeration<? extends ZipEntry> e= jar.entries(); e.hasMoreElements();) { 188 ZipEntry member= e.nextElement(); 189 String name = member.getName(); 190 if (this.multiVersion && name.length() > (length + 2) && name.startsWith(version)) { 191 int end = name.indexOf('/', length); 192 if (end >= name.length()) continue; 193 String versionPath = name.substring(0, end); 194 String ver = name.substring(length, end); 195 if(versions.contains(ver) && org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(name)) { 196 name = name.substring(end + 1); 197 overridden.put(name, versionPath); 198 } 199 } 200 initRawPackageInfo(rawPackageInfo, name, member.isDirectory(), CompilerOptions.versionFromJdkLevel(classLevel)); 201 } 202 } finally { 203 JavaModelManager.getJavaModelManager().closeZipFile(jar); 204 } 205 } 206 // loop through all of referenced packages, creating package fragments if necessary 207 // and cache the entry names in the rawPackageInfo table 208 children = new IJavaElement[rawPackageInfo.size()]; 209 int index = 0; 210 for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) { 211 String[] pkgName = (String[]) rawPackageInfo.keyTable[i]; 212 if (pkgName == null) continue; 213 children[index++] = getPackageFragment(pkgName); 214 } 215 } catch (CoreException e) { 216 if (e.getCause() instanceof ZipException) { 217 // not a ZIP archive, leave the children empty 218 Util.log(IStatus.ERROR, "Invalid ZIP archive: " + toStringWithAncestors()); //$NON-NLS-1$ 219 children = NO_ELEMENTS; 220 } else if (e instanceof JavaModelException) { 221 throw (JavaModelException)e; 222 } else { 223 throw new JavaModelException(e); 224 } 225 } 226 info.setChildren(children); 227 ((JarPackageFragmentRootInfo) info).rawPackageInfo = rawPackageInfo; 228 ((JarPackageFragmentRootInfo) info).overriddenClasses = overridden; 229 return true; 230 } createChildren(final HashtableOfArrayToObject rawPackageInfo)231 protected IJavaElement[] createChildren(final HashtableOfArrayToObject rawPackageInfo) { 232 IJavaElement[] children; 233 // loop through all of referenced packages, creating package fragments if necessary 234 // and cache the entry names in the rawPackageInfo table 235 children = new IJavaElement[rawPackageInfo.size()]; 236 int index = 0; 237 for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) { 238 String[] pkgName = (String[]) rawPackageInfo.keyTable[i]; 239 if (pkgName == null) continue; 240 children[index++] = getPackageFragment(pkgName); 241 } 242 return children; 243 } 244 /** 245 * Returns a new element info for this element. 246 */ 247 @Override createElementInfo()248 protected Object createElementInfo() { 249 return new JarPackageFragmentRootInfo(); 250 } 251 /** 252 * A Jar is always K_BINARY. 253 */ 254 @Override determineKind(IResource underlyingResource)255 protected int determineKind(IResource underlyingResource) { 256 return IPackageFragmentRoot.K_BINARY; 257 } 258 /** 259 * Returns true if this handle represents the same jar 260 * as the given handle. Two jars are equal if they share 261 * the same zip file. 262 * 263 * @see Object#equals 264 */ 265 @Override equals(Object o)266 public boolean equals(Object o) { 267 if (this == o) 268 return true; 269 if (o instanceof JarPackageFragmentRoot) { 270 JarPackageFragmentRoot other= (JarPackageFragmentRoot) o; 271 return this.jarPath.equals(other.jarPath) 272 && Arrays.equals(this.extraAttributes, other.extraAttributes); 273 } 274 return false; 275 } 276 @Override getElementName()277 public String getElementName() { 278 return this.jarPath.lastSegment(); 279 } 280 /** 281 * Returns the underlying ZipFile for this Jar package fragment root. 282 * 283 * @exception CoreException if an error occurs accessing the jar 284 */ getJar()285 public ZipFile getJar() throws CoreException { 286 return JavaModelManager.getJavaModelManager().getZipFile(getPath()); 287 } 288 /** 289 * @see IPackageFragmentRoot 290 */ 291 @Override getKind()292 public int getKind() { 293 return IPackageFragmentRoot.K_BINARY; 294 } 295 @Override internalKind()296 int internalKind() throws JavaModelException { 297 return IPackageFragmentRoot.K_BINARY; 298 } 299 /** 300 * Returns an array of non-java resources contained in the receiver. 301 */ 302 @Override getNonJavaResources()303 public Object[] getNonJavaResources() throws JavaModelException { 304 // We want to show non java resources of the default package at the root (see PR #1G58NB8) 305 Object[] defaultPkgResources = ((JarPackageFragment) getPackageFragment(CharOperation.NO_STRINGS)).storedNonJavaResources(); 306 int length = defaultPkgResources.length; 307 if (length == 0) 308 return defaultPkgResources; 309 Object[] nonJavaResources = new Object[length]; 310 for (int i = 0; i < length; i++) { 311 JarEntryResource nonJavaResource = (JarEntryResource) defaultPkgResources[i]; 312 nonJavaResources[i] = nonJavaResource.clone(this); 313 } 314 return nonJavaResources; 315 } 316 @Override getPackageFragment(String[] pkgName)317 public PackageFragment getPackageFragment(String[] pkgName) { 318 return new JarPackageFragment(this, pkgName); 319 } 320 @Override getPackageFragment(String[] pkgName, String mod)321 public PackageFragment getPackageFragment(String[] pkgName, String mod) { 322 return new JarPackageFragment(this, pkgName); // Overridden in JImageModuleFragmentBridge 323 } 324 325 @Override getClassFilePath(String classname)326 public String getClassFilePath(String classname) { 327 if (this.multiVersion) { 328 JarPackageFragmentRootInfo elementInfo; 329 try { 330 elementInfo = (JarPackageFragmentRootInfo) getElementInfo(); 331 String versionPath = elementInfo.overriddenClasses.get(classname); 332 return versionPath == null ? classname : versionPath + '/' + classname; 333 } catch (JavaModelException e) { 334 // move on 335 } 336 } 337 return classname; 338 } 339 @Override getModuleDescription()340 public IModuleDescription getModuleDescription() { 341 if (this.knownToBeModuleLess) 342 return null; 343 IModuleDescription module = super.getModuleDescription(); 344 if (module == null) 345 this.knownToBeModuleLess = true; 346 return module; 347 } 348 349 @Override internalPath()350 public IPath internalPath() { 351 if (isExternal()) { 352 return this.jarPath; 353 } else { 354 return super.internalPath(); 355 } 356 } 357 @Override resource(PackageFragmentRoot root)358 public IResource resource(PackageFragmentRoot root) { 359 if (this.resource == null) { 360 // external jar 361 return null; 362 } 363 return super.resource(root); 364 } 365 366 367 /** 368 * @see IJavaElement 369 */ 370 @Override getUnderlyingResource()371 public IResource getUnderlyingResource() throws JavaModelException { 372 if (isExternal()) { 373 if (!exists()) throw newNotPresentException(); 374 return null; 375 } else { 376 return super.getUnderlyingResource(); 377 } 378 } 379 @Override hashCode()380 public int hashCode() { 381 return this.jarPath.hashCode() + Arrays.hashCode(this.extraAttributes); 382 } initRawPackageInfo(HashtableOfArrayToObject rawPackageInfo, String entryName, boolean isDirectory, String compliance)383 protected void initRawPackageInfo(HashtableOfArrayToObject rawPackageInfo, String entryName, boolean isDirectory, String compliance) { 384 int lastSeparator; 385 if (isDirectory) { 386 if (entryName.charAt(entryName.length() - 1) == '/') { 387 lastSeparator = entryName.length() - 1; 388 } else { 389 lastSeparator = entryName.length(); 390 } 391 } else { 392 lastSeparator = entryName.lastIndexOf('/'); 393 } 394 String[] pkgName = Util.splitOn('/', entryName, 0, lastSeparator); 395 String[] existing = null; 396 int length = pkgName.length; 397 int existingLength = length; 398 while (existingLength >= 0) { 399 existing = (String[]) rawPackageInfo.getKey(pkgName, existingLength); 400 if (existing != null) break; 401 existingLength--; 402 } 403 JavaModelManager manager = JavaModelManager.getJavaModelManager(); 404 for (int i = existingLength; i < length; i++) { 405 // sourceLevel must be null because we know nothing about it based on a jar file 406 if (Util.isValidFolderNameForPackage(pkgName[i], null, compliance)) { 407 System.arraycopy(existing, 0, existing = new String[i+1], 0, i); 408 existing[i] = manager.intern(pkgName[i]); 409 rawPackageInfo.put(existing, new ArrayList[] { EMPTY_LIST, EMPTY_LIST }); 410 } else { 411 // non-Java resource folder 412 if (!isDirectory) { 413 ArrayList[] children = (ArrayList[]) rawPackageInfo.get(existing); 414 if (children[1/*NON_JAVA*/] == EMPTY_LIST) children[1/*NON_JAVA*/] = new ArrayList(); 415 children[1/*NON_JAVA*/].add(entryName); 416 } 417 return; 418 } 419 } 420 if (isDirectory) 421 return; 422 423 // add classfile info amongst children 424 ArrayList[] children = (ArrayList[]) rawPackageInfo.get(pkgName); 425 if (org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(entryName)) { 426 if (children[0/*JAVA*/] == EMPTY_LIST) children[0/*JAVA*/] = new ArrayList(); 427 String nameWithoutExtension = entryName.substring(lastSeparator + 1, entryName.length() - 6); 428 children[0/*JAVA*/].add(nameWithoutExtension); 429 } else { 430 if (children[1/*NON_JAVA*/] == EMPTY_LIST) children[1/*NON_JAVA*/] = new ArrayList(); 431 children[1/*NON_JAVA*/].add(entryName); 432 } 433 434 } 435 /** 436 * @see IPackageFragmentRoot 437 */ 438 @Override isArchive()439 public boolean isArchive() { 440 return true; 441 } 442 /** 443 * @see IPackageFragmentRoot 444 */ 445 @Override isExternal()446 public boolean isExternal() { 447 return resource() == null; 448 } 449 /** 450 * Jars and jar entries are all read only 451 */ 452 @Override isReadOnly()453 public boolean isReadOnly() { 454 return true; 455 } 456 /** 457 * Returns whether the corresponding resource or associated file exists 458 */ 459 @Override resourceExists(IResource underlyingResource)460 protected boolean resourceExists(IResource underlyingResource) { 461 if (underlyingResource == null) { 462 return 463 JavaModel.getExternalTarget( 464 getPath()/*don't make the path relative as this is an external archive*/, 465 true/*check existence*/) != null; 466 } else { 467 return super.resourceExists(underlyingResource); 468 } 469 } 470 471 @Override toStringAncestors(StringBuffer buffer)472 protected void toStringAncestors(StringBuffer buffer) { 473 if (isExternal()) 474 // don't show project as it is irrelevant for external jar files. 475 // also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=146615 476 return; 477 super.toStringAncestors(buffer); 478 } 479 getIndexPath()480 public URL getIndexPath() { 481 try { 482 IClasspathEntry entry = ((JavaProject) getParent()).getClasspathEntryFor(getPath()); 483 if (entry != null) return ((ClasspathEntry)entry).getLibraryIndexLocation(); 484 } catch (JavaModelException e) { 485 // ignore exception 486 } 487 return null; 488 } 489 490 @Override getManifest()491 public Manifest getManifest() { 492 ZipFile jar = null; 493 try { 494 jar = getJar(); 495 ZipEntry mfEntry = jar.getEntry(TypeConstants.META_INF_MANIFEST_MF); 496 if (mfEntry != null) { 497 try (InputStream is = jar.getInputStream(mfEntry)) { 498 return new Manifest(is); 499 } 500 } 501 } catch (CoreException | IOException e) { 502 // must do without manifest 503 } finally { 504 JavaModelManager.getJavaModelManager().closeZipFile(jar); 505 } 506 return null; 507 } 508 509 // @Override 510 // public boolean isModule() { 511 // try { 512 // return ((PackageFragmentRootInfo) getElementInfo()).isModule(resource(), this); 513 // } catch (JavaModelException e) { 514 // return false; 515 // } 516 // } 517 } 518