org.eclipse.jdt.internal.core.JarPackageFragmentRoot.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jdt.internal.core.JarPackageFragmentRoot.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2019 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IModuleDescription;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.core.JavaModelManager.PerProjectInfo;
import org.eclipse.jdt.internal.core.nd.IReader;
import org.eclipse.jdt.internal.core.nd.java.JavaIndex;
import org.eclipse.jdt.internal.core.nd.java.NdResourceFile;
import org.eclipse.jdt.internal.core.nd.java.NdType;
import org.eclipse.jdt.internal.core.nd.java.NdZipEntry;
import org.eclipse.jdt.internal.core.util.HashtableOfArrayToObject;
import org.eclipse.jdt.internal.core.util.Util;

/**
 * A package fragment root that corresponds to a .jar or .zip.
 *
 * <p>NOTE: The only visible entries from a .jar or .zip package fragment root
 * are .class files.
 * <p>NOTE: A jar package fragment root may or may not have an associated resource.
 *
 * @see org.eclipse.jdt.core.IPackageFragmentRoot
 * @see org.eclipse.jdt.internal.core.JarPackageFragmentRootInfo
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
public class JarPackageFragmentRoot extends PackageFragmentRoot {

    protected final static ArrayList EMPTY_LIST = new ArrayList();

    /**
     * The path to the jar file
     * (a workspace relative path if the jar is internal,
     * or an OS path if the jar is external)
     */
    protected final IPath jarPath;

    boolean knownToBeModuleLess;

    private boolean multiVersion;

    /**
     * Reflects the extra attributes of the classpath entry declaring this root.
     * Caution, this field is used in hashCode() & equals() to avoid overzealous sharing.
     * Can be null, if lookup via the corresponding classpath entry failed.
     */
    final protected IClasspathAttribute[] extraAttributes;

    /**
     * Constructs a package fragment root which is the root of the Java package directory hierarchy
     * based on a JAR file.
     */
    public JarPackageFragmentRoot(IResource resource, IPath externalJarPath, JavaProject project,
            IClasspathAttribute[] attributes) {
        super(resource, project);
        this.jarPath = externalJarPath;
        if (attributes == null) {
            // attributes could either be 
            // (1) provided by the caller (particularly when creating from memento),
            // (2) retrieved from the corresponding classpath entry (if a resolved classpath is available).
            // These two cases should cover all normal scenarios, else extraAttributes will be null.
            try {
                PerProjectInfo perProjectInfo = project.getPerProjectInfo();
                synchronized (perProjectInfo) {
                    if (perProjectInfo.resolvedClasspath != null
                            && perProjectInfo.unresolvedEntryStatus == JavaModelStatus.VERIFIED_OK) {
                        IClasspathEntry classpathEntry = project.getClasspathEntryFor(externalJarPath);
                        if (classpathEntry != null)
                            attributes = classpathEntry.getExtraAttributes();
                    }
                }
            } catch (JavaModelException e) {
                // ignore
            }
        }
        this.extraAttributes = attributes;
    }

    /**
     * Compute the package fragment children of this package fragment root.
     * These are all of the directory zip entries, and any directories implied
     * by the path of class files contained in the jar of this package fragment root.
     */
    @Override
    protected boolean computeChildren(OpenableElementInfo info, IResource underlyingResource)
            throws JavaModelException {
        final HashtableOfArrayToObject rawPackageInfo = new HashtableOfArrayToObject();
        final Map<String, String> overridden = new HashMap<>();
        IJavaElement[] children = NO_ELEMENTS;
        try {
            // always create the default package
            rawPackageInfo.put(CharOperation.NO_STRINGS, new ArrayList[] { EMPTY_LIST, EMPTY_LIST });

            boolean usedIndex = false;
            if (JavaIndex.isEnabled()) {
                JavaIndex index = JavaIndex.getIndex();
                try (IReader reader = index.getNd().acquireReadLock()) {
                    IPath resourcePath = JavaIndex.getLocationForElement(this);
                    if (!resourcePath.isEmpty()) {
                        NdResourceFile resourceFile = index.getResourceFile(resourcePath.toString().toCharArray());
                        if (index.isUpToDate(resourceFile)) {
                            usedIndex = true;
                            long level = resourceFile.getJdkLevel();
                            String compliance = CompilerOptions.versionFromJdkLevel(level);
                            // Locate all the non-classfile entries
                            for (NdZipEntry next : resourceFile.getZipEntries()) {
                                String filename = next.getFileName().getString();
                                initRawPackageInfo(rawPackageInfo, filename, filename.endsWith("/"), compliance); //$NON-NLS-1$
                            }

                            // Locate all the classfile entries
                            for (NdType type : resourceFile.getTypes()) {
                                String path = new String(type.getTypeId().getBinaryName()) + ".class"; //$NON-NLS-1$
                                initRawPackageInfo(rawPackageInfo, path, false, compliance);
                            }
                        }
                    }
                }
            }

            // If we weren't able to compute the set of children from the index (either the index was disabled or didn't
            // contain an up-to-date entry for this .jar) then fetch it directly from the .jar
            if (!usedIndex) {
                Object file = JavaModel.getTarget(getPath(), true);
                long classLevel = Util.getJdkLevel(file);
                String projectCompliance = this.getJavaProject().getOption(JavaCore.COMPILER_COMPLIANCE, true);
                long projectLevel = CompilerOptions.versionToJdkLevel(projectCompliance);
                ZipFile jar = null;
                try {
                    jar = getJar();
                    String version = "META-INF/versions/"; //$NON-NLS-1$
                    List<String> versions = new ArrayList<>();
                    if (projectLevel >= ClassFileConstants.JDK9 && jar.getEntry(version) != null) {
                        int earliestJavaVersion = ClassFileConstants.MAJOR_VERSION_9;
                        long latestJDK = CompilerOptions.releaseToJDKLevel(projectCompliance);
                        int latestJavaVer = (int) (latestJDK >> 16);

                        for (int i = latestJavaVer; i >= earliestJavaVersion; i--) {
                            String s = "" + +(i - 44); //$NON-NLS-1$
                            String versionPath = version + s;
                            if (jar.getEntry(versionPath) != null) {
                                versions.add(s);
                            }
                        }
                    }

                    String[] supportedVersions = versions.toArray(new String[versions.size()]);
                    if (supportedVersions.length > 0) {
                        this.multiVersion = true;
                    }
                    int length = version.length();
                    for (Enumeration<? extends ZipEntry> e = jar.entries(); e.hasMoreElements();) {
                        ZipEntry member = e.nextElement();
                        String name = member.getName();
                        if (this.multiVersion && name.length() > (length + 2) && name.startsWith(version)) {
                            int end = name.indexOf('/', length);
                            if (end >= name.length())
                                continue;
                            String versionPath = name.substring(0, end);
                            String ver = name.substring(length, end);
                            if (versions.contains(ver)
                                    && org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(name)) {
                                name = name.substring(end + 1);
                                overridden.put(name, versionPath);
                            }
                        }
                        initRawPackageInfo(rawPackageInfo, name, member.isDirectory(),
                                CompilerOptions.versionFromJdkLevel(classLevel));
                    }
                } finally {
                    JavaModelManager.getJavaModelManager().closeZipFile(jar);
                }
            }
            // loop through all of referenced packages, creating package fragments if necessary
            // and cache the entry names in the rawPackageInfo table
            children = new IJavaElement[rawPackageInfo.size()];
            int index = 0;
            for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) {
                String[] pkgName = (String[]) rawPackageInfo.keyTable[i];
                if (pkgName == null)
                    continue;
                children[index++] = getPackageFragment(pkgName);
            }
        } catch (CoreException e) {
            if (e.getCause() instanceof ZipException) {
                // not a ZIP archive, leave the children empty
                Util.log(IStatus.ERROR, "Invalid ZIP archive: " + toStringWithAncestors()); //$NON-NLS-1$
                children = NO_ELEMENTS;
            } else if (e instanceof JavaModelException) {
                throw (JavaModelException) e;
            } else {
                throw new JavaModelException(e);
            }
        }
        info.setChildren(children);
        ((JarPackageFragmentRootInfo) info).rawPackageInfo = rawPackageInfo;
        ((JarPackageFragmentRootInfo) info).overriddenClasses = overridden;
        return true;
    }

    protected IJavaElement[] createChildren(final HashtableOfArrayToObject rawPackageInfo) {
        IJavaElement[] children;
        // loop through all of referenced packages, creating package fragments if necessary
        // and cache the entry names in the rawPackageInfo table
        children = new IJavaElement[rawPackageInfo.size()];
        int index = 0;
        for (int i = 0, length = rawPackageInfo.keyTable.length; i < length; i++) {
            String[] pkgName = (String[]) rawPackageInfo.keyTable[i];
            if (pkgName == null)
                continue;
            children[index++] = getPackageFragment(pkgName);
        }
        return children;
    }

    /**
     * Returns a new element info for this element.
     */
    @Override
    protected Object createElementInfo() {
        return new JarPackageFragmentRootInfo();
    }

    /**
     * A Jar is always K_BINARY.
     */
    @Override
    protected int determineKind(IResource underlyingResource) {
        return IPackageFragmentRoot.K_BINARY;
    }

    /**
     * Returns true if this handle represents the same jar
     * as the given handle. Two jars are equal if they share
     * the same zip file.
     *
     * @see Object#equals
     */
    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o instanceof JarPackageFragmentRoot) {
            JarPackageFragmentRoot other = (JarPackageFragmentRoot) o;
            return this.jarPath.equals(other.jarPath) && Arrays.equals(this.extraAttributes, other.extraAttributes);
        }
        return false;
    }

    @Override
    public String getElementName() {
        return this.jarPath.lastSegment();
    }

    /**
     * Returns the underlying ZipFile for this Jar package fragment root.
     *
     * @exception CoreException if an error occurs accessing the jar
     */
    public ZipFile getJar() throws CoreException {
        return JavaModelManager.getJavaModelManager().getZipFile(getPath());
    }

    /**
     * @see IPackageFragmentRoot
     */
    @Override
    public int getKind() {
        return IPackageFragmentRoot.K_BINARY;
    }

    @Override
    int internalKind() throws JavaModelException {
        return IPackageFragmentRoot.K_BINARY;
    }

    /**
     * Returns an array of non-java resources contained in the receiver.
     */
    @Override
    public Object[] getNonJavaResources() throws JavaModelException {
        // We want to show non java resources of the default package at the root (see PR #1G58NB8)
        Object[] defaultPkgResources = ((JarPackageFragment) getPackageFragment(CharOperation.NO_STRINGS))
                .storedNonJavaResources();
        int length = defaultPkgResources.length;
        if (length == 0)
            return defaultPkgResources;
        Object[] nonJavaResources = new Object[length];
        for (int i = 0; i < length; i++) {
            JarEntryResource nonJavaResource = (JarEntryResource) defaultPkgResources[i];
            nonJavaResources[i] = nonJavaResource.clone(this);
        }
        return nonJavaResources;
    }

    @Override
    public PackageFragment getPackageFragment(String[] pkgName) {
        return new JarPackageFragment(this, pkgName);
    }

    @Override
    public PackageFragment getPackageFragment(String[] pkgName, String mod) {
        return new JarPackageFragment(this, pkgName); // Overridden in JImageModuleFragmentBridge
    }

    @Override
    public String getClassFilePath(String classname) {
        if (this.multiVersion) {
            JarPackageFragmentRootInfo elementInfo;
            try {
                elementInfo = (JarPackageFragmentRootInfo) getElementInfo();
                String versionPath = elementInfo.overriddenClasses.get(classname);
                return versionPath == null ? classname : versionPath + '/' + classname;
            } catch (JavaModelException e) {
                // move on
            }
        }
        return classname;
    }

    @Override
    public IModuleDescription getModuleDescription() {
        if (this.knownToBeModuleLess)
            return null;
        IModuleDescription module = super.getModuleDescription();
        if (module == null)
            this.knownToBeModuleLess = true;
        return module;
    }

    @Override
    public IPath internalPath() {
        if (isExternal()) {
            return this.jarPath;
        } else {
            return super.internalPath();
        }
    }

    @Override
    public IResource resource(PackageFragmentRoot root) {
        if (this.resource == null) {
            // external jar
            return null;
        }
        return super.resource(root);
    }

    /**
     * @see IJavaElement
     */
    @Override
    public IResource getUnderlyingResource() throws JavaModelException {
        if (isExternal()) {
            if (!exists())
                throw newNotPresentException();
            return null;
        } else {
            return super.getUnderlyingResource();
        }
    }

    @Override
    public int hashCode() {
        return this.jarPath.hashCode() + Arrays.hashCode(this.extraAttributes);
    }

    protected void initRawPackageInfo(HashtableOfArrayToObject rawPackageInfo, String entryName,
            boolean isDirectory, String compliance) {
        int lastSeparator;
        if (isDirectory) {
            if (entryName.charAt(entryName.length() - 1) == '/') {
                lastSeparator = entryName.length() - 1;
            } else {
                lastSeparator = entryName.length();
            }
        } else {
            lastSeparator = entryName.lastIndexOf('/');
        }
        String[] pkgName = Util.splitOn('/', entryName, 0, lastSeparator);
        String[] existing = null;
        int length = pkgName.length;
        int existingLength = length;
        while (existingLength >= 0) {
            existing = (String[]) rawPackageInfo.getKey(pkgName, existingLength);
            if (existing != null)
                break;
            existingLength--;
        }
        JavaModelManager manager = JavaModelManager.getJavaModelManager();
        for (int i = existingLength; i < length; i++) {
            // sourceLevel must be null because we know nothing about it based on a jar file
            if (Util.isValidFolderNameForPackage(pkgName[i], null, compliance)) {
                System.arraycopy(existing, 0, existing = new String[i + 1], 0, i);
                existing[i] = manager.intern(pkgName[i]);
                rawPackageInfo.put(existing, new ArrayList[] { EMPTY_LIST, EMPTY_LIST });
            } else {
                // non-Java resource folder
                if (!isDirectory) {
                    ArrayList[] children = (ArrayList[]) rawPackageInfo.get(existing);
                    if (children[1/*NON_JAVA*/] == EMPTY_LIST)
                        children[1/*NON_JAVA*/] = new ArrayList();
                    children[1/*NON_JAVA*/].add(entryName);
                }
                return;
            }
        }
        if (isDirectory)
            return;

        // add classfile info amongst children
        ArrayList[] children = (ArrayList[]) rawPackageInfo.get(pkgName);
        if (org.eclipse.jdt.internal.compiler.util.Util.isClassFileName(entryName)) {
            if (children[0/*JAVA*/] == EMPTY_LIST)
                children[0/*JAVA*/] = new ArrayList();
            String nameWithoutExtension = entryName.substring(lastSeparator + 1, entryName.length() - 6);
            children[0/*JAVA*/].add(nameWithoutExtension);
        } else {
            if (children[1/*NON_JAVA*/] == EMPTY_LIST)
                children[1/*NON_JAVA*/] = new ArrayList();
            children[1/*NON_JAVA*/].add(entryName);
        }

    }

    /**
     * @see IPackageFragmentRoot
     */
    @Override
    public boolean isArchive() {
        return true;
    }

    /**
     * @see IPackageFragmentRoot
     */
    @Override
    public boolean isExternal() {
        return resource() == null;
    }

    /**
     * Jars and jar entries are all read only
     */
    @Override
    public boolean isReadOnly() {
        return true;
    }

    /**
     * Returns whether the corresponding resource or associated file exists
     */
    @Override
    protected boolean resourceExists(IResource underlyingResource) {
        if (underlyingResource == null) {
            return JavaModel.getExternalTarget(
                    getPath()/*don't make the path relative as this is an external archive*/,
                    true/*check existence*/) != null;
        } else {
            return super.resourceExists(underlyingResource);
        }
    }

    @Override
    protected void toStringAncestors(StringBuffer buffer) {
        if (isExternal())
            // don't show project as it is irrelevant for external jar files.
            // also see https://bugs.eclipse.org/bugs/show_bug.cgi?id=146615
            return;
        super.toStringAncestors(buffer);
    }

    public URL getIndexPath() {
        try {
            IClasspathEntry entry = ((JavaProject) getParent()).getClasspathEntryFor(getPath());
            if (entry != null)
                return ((ClasspathEntry) entry).getLibraryIndexLocation();
        } catch (JavaModelException e) {
            // ignore exception
        }
        return null;
    }

    @Override
    public Manifest getManifest() {
        ZipFile jar = null;
        try {
            jar = getJar();
            ZipEntry mfEntry = jar.getEntry(TypeConstants.META_INF_MANIFEST_MF);
            if (mfEntry != null)
                return new Manifest(jar.getInputStream(mfEntry));
        } catch (CoreException | IOException e) {
            // must do without manifest
        } finally {
            JavaModelManager.getJavaModelManager().closeZipFile(jar);
        }
        return null;
    }

    //   @Override
    //   public boolean isModule() {
    //       try {
    //          return ((PackageFragmentRootInfo) getElementInfo()).isModule(resource(), this);
    //       } catch (JavaModelException e) {
    //          return false;
    //       }
    //   }
}