--- a/java.project/nbproject/project.xml +++ a/java.project/nbproject/project.xml @@ -128,6 +128,14 @@ + org.netbeans.modules.java.source + + + + 0.118 + + + org.netbeans.modules.project.ant --- a/java.project/src/org/netbeans/modules/java/project/ProfileProblemsProviderImpl.java +++ a/java.project/src/org/netbeans/modules/java/project/ProfileProblemsProviderImpl.java @@ -45,28 +45,22 @@ import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.net.URI; import java.net.URL; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; -import java.util.jar.Attributes; -import java.util.jar.Manifest; -import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.JButton; @@ -78,6 +72,7 @@ import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.queries.SourceLevelQuery; import org.netbeans.api.java.queries.SourceLevelQuery.Profile; +import org.netbeans.api.java.source.support.ProfileSupport; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; @@ -89,7 +84,6 @@ import org.netbeans.spi.project.support.ant.ReferenceHelper; import org.netbeans.spi.project.ui.ProjectProblemsProvider; import org.netbeans.spi.project.ui.support.ProjectProblemsProviderSupport; -import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Mutex; import org.openide.util.NbBundle; @@ -105,10 +99,8 @@ import org.netbeans.spi.java.classpath.ClassPathFactory; import org.netbeans.spi.java.project.classpath.support.ProjectClassPathSupport; import org.netbeans.spi.project.support.ant.PropertyUtils; -import org.openide.filesystems.URLMapper; import org.openide.util.ImageUtilities; import org.openide.util.RequestProcessor; -import org.openide.util.Union2; /** * * @author Tomas Zezula @@ -326,11 +318,17 @@ final String libName = rawEntry.substring(LIB_PREFIX.length(),rawEntry.lastIndexOf('.')); final Library lib = refHelper.findLibrary(libName); if (lib != null) { - final Union2 minProfileOrName = findProfile(lib.getContent(VOL_CLASSPATH)); - final Profile minProfile = minProfileOrName.hasFirst() ? minProfileOrName.first() : null; - if (isBroken(requiredProfile, minProfile)) { - collector.add(new LibraryReference(classPathId, rawEntry, minProfile, lib)); - } + final Collection res = + ProfileSupport.findProfileViolations( + requiredProfile, + Collections.emptySet(), + lib.getContent(VOL_CLASSPATH), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST)); + if (!res.isEmpty()) { + final Profile maxProfile = findMaxProfile(res); + collector.add(new LibraryReference(classPathId, rawEntry, maxProfile, lib)); + } } } else if (rawEntry.startsWith(PRJ_PREFIX)) { final Object[] ref = refHelper.findArtifactAndLocation(rawEntry); @@ -350,10 +348,16 @@ final File file = antProjectHelper.resolveFile(path); final URL root = FileUtil.urlForArchiveOrDir(file); if (root != null) { - final Union2 minProfileOrName = findProfile(root); - final Profile minProfile = minProfileOrName.hasFirst() ? minProfileOrName.first() : null; - if (isBroken(requiredProfile, minProfile)) { - collector.add(new FileReference(classPathId, rawEntry, minProfile, file)); + final Collection res = + ProfileSupport.findProfileViolations( + requiredProfile, + Collections.emptySet(), + Collections.singleton(root), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST)); + if (!res.isEmpty()) { + final Profile maxProfile = findMaxProfile(res); + collector.add(new FileReference(classPathId, rawEntry, maxProfile, file)); } } } @@ -373,10 +377,16 @@ final File file = antProjectHelper.resolveFile(propName); final URL root = FileUtil.urlForArchiveOrDir(file); if (root != null) { - final Union2 minProfileOrName = findProfile(root); - final Profile minProfile = minProfileOrName.hasFirst() ? minProfileOrName.first() : null; - if (isBroken(requiredProfile, minProfile)) { - collector.add(new FileReference(classPathId, rawEntry, minProfile, file)); + final Collection res = + ProfileSupport.findProfileViolations( + requiredProfile, + Collections.emptySet(), + Collections.singleton(root), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST)); + if (!res.isEmpty()) { + final Profile maxProfile = findMaxProfile(res); + collector.add(new FileReference(classPathId, rawEntry, maxProfile, file)); } } } @@ -405,60 +415,20 @@ return reference; } - @NonNull - private static Union2 findProfile(@NonNull final Iterable roots) { - Profile current = Profile.DEFAULT; - for (URL root : roots) { - final Union2 rootProfile = findProfile(root); - if (!rootProfile.hasFirst()) { + @CheckForNull + private static Profile findMaxProfile(@NonNull final Iterable violations) { + Profile current = null; + for (ProfileSupport.Violation violation : violations) { + Profile p = violation.getRequiredProfile(); + if (p == null) { //Broken profile - no need to continue - return rootProfile; + return null; } - current = max(current, rootProfile.first()); + current = max(current, p); } - return Union2.createFirst(current); + return current; } - - @NonNull - private static Union2 findProfile(@NonNull URL root) { - Union2 res; - final ArchiveCache.Key key = ArchiveCache.createKey(root); - if (key != null) { - res = ArchiveCache.getProfile(key); - if (res != null) { - return res; - } - } - String profileName = null; - final FileObject rootFo = URLMapper.findFileObject(root); - if (rootFo != null) { - final FileObject manifestFile = rootFo.getFileObject(RES_MANIFEST); - if (manifestFile != null) { - try { - try (InputStream in = manifestFile.getInputStream()) { - final Manifest manifest = new Manifest(in); - final Attributes attrs = manifest.getMainAttributes(); - profileName = attrs.getValue(ATTR_PROFILE); - } - } catch (IOException ioe) { - LOG.log( - Level.INFO, - "Cannot read Profile attribute from: {0}", //NOI18N - FileUtil.getFileDisplayName(manifestFile)); - } - } - } - final Profile profile = Profile.forName(profileName); - res = profile != null ? - Union2.createFirst(profile) : - Union2.createSecond(profileName); - if (key != null) { - ArchiveCache.putProfile(key, res); - } - return res; - } - - + @CheckForNull static Profile max (@NullAllowed Profile a, @NullAllowed Profile b) { if (b == null) { @@ -745,118 +715,5 @@ res.run(); return res; } - } - - - private static final class ArchiveCache { - - private static final int MAX_CACHE_SIZE = Integer.getInteger( - "ProfileProblemsProviderImpl.ArchiveCache.size", //NOI18N - 1<<10); - - private static final Map> cache = new LinkedHashMap>(16, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry> entry) { - return size() > MAX_CACHE_SIZE; - } - }; - - private ArchiveCache() {} - - @CheckForNull - static Union2 getProfile(@NonNull final Key key) { - final Union2 res = cache.get(key); - if (LOG.isLoggable(Level.FINER)) { - LOG.log( - Level.FINER, - "cache[{0}]->{1}", //NOI18N - new Object[]{ - key, - res.hasFirst() ? res.first() : res.second() - }); - } - return res; - } - - static void putProfile( - @NonNull final Key key, - @NonNull final Union2 profile) { - if (LOG.isLoggable(Level.FINER)) { - LOG.log( - Level.FINER, - "cache[{0}]<-{1}", //NOI18N - new Object[]{ - key, - profile.hasFirst() ? profile.first() : profile.second() - }); - } - cache.put(key,profile); - } - - @CheckForNull - static Key createKey(@NonNull final URL rootURL) { - final URL fileURL = FileUtil.getArchiveFile(rootURL); - if (fileURL == null) { - //Not an archive - return null; - } - final FileObject fileFo = URLMapper.findFileObject(fileURL); - if (fileFo == null) { - return null; - } - return new Key( - fileFo.toURI(), - fileFo.lastModified().getTime(), - fileFo.getSize()); - } - - private static final class Key { - - private final URI root; - private final long mtime; - private final long size; - - Key( - @NonNull final URI root, - final long mtime, - final long size) { - this.root = root; - this.mtime = mtime; - this.size = size; - } - - @Override - public int hashCode() { - int hash = 17; - hash = 31 * hash + (this.root != null ? this.root.hashCode() : 0); - hash = 31 * hash + (int) (this.mtime ^ (this.mtime >>> 32)); - hash = 31 * hash + (int) (this.size ^ (this.size >>> 32)); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - if (!(obj instanceof Key)) { - return false; - } - final Key other = (Key) obj; - return this.root.equals(other.root) && - this.mtime == other.mtime && - this.size == other.size; - - } - - @Override - public String toString() { - return String.format( - "Key{root: %s, mtime: %d, size: %d}", //NOI18N - root, - mtime, - size); - } - } - } + } } --- a/java.source/apichanges.xml +++ a/java.source/apichanges.xml @@ -108,6 +108,19 @@ + + + Added support for JDK 8 Profiles. + + + + + + Added ProfileSupport class providing utility methods for JDK 8 profiles. + + + + Added several methods to support Java 8 features. --- a/java.source/nbproject/project.properties +++ a/java.source/nbproject/project.properties @@ -46,7 +46,7 @@ javadoc.title=Java Source javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=0.118.0 +spec.version.base=0.119.0 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\ ${o.n.core.dir}/lib/boot.jar:\ --- a/java.source/src/org/netbeans/api/java/source/support/ProfileSupport.java +++ a/java.source/src/org/netbeans/api/java/source/support/ProfileSupport.java @@ -0,0 +1,848 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.api.java.source.support; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.java.queries.SourceLevelQuery.Profile; +import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.modules.classfile.Annotation; +import org.netbeans.modules.classfile.AnnotationComponent; +import org.netbeans.modules.classfile.CPEntry; +import org.netbeans.modules.classfile.ClassFile; +import org.netbeans.modules.classfile.ClassName; +import org.netbeans.modules.classfile.ElementValue; +import org.netbeans.modules.classfile.PrimitiveElementValue; +import org.netbeans.modules.java.source.ElementHandleAccessor; +import org.netbeans.modules.java.source.indexing.JavaIndex; +import org.netbeans.modules.java.source.parsing.Archive; +import org.netbeans.modules.java.source.parsing.CachingArchiveProvider; +import org.netbeans.modules.java.source.parsing.FileObjects; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.filesystems.URLMapper; +import org.openide.util.Exceptions; +import org.openide.util.Parameters; +import org.openide.util.RequestProcessor; +import org.openide.util.Union2; +import org.openide.util.Utilities; + +/** + * Utility methods for JDK 8 Profiles. + * @author Tomas Zezula + * @since 0.119 + */ +public class ProfileSupport { + + private static final String RES_MANIFEST = "META-INF/MANIFEST.MF"; //NOI18N + private static final String ATTR_PROFILE = "Profile"; //NOI18N + private static final String ANNOTATION_PROFILE = "jdk/Profile+Annotation"; //NOI18N + private static final String ANNOTATION_VALUE = "value"; //NOI18N + private static final Logger LOG = Logger.getLogger(ProfileSupport.class.getName()); + private static final RequestProcessor RP = new RequestProcessor(ProfileSupport.class); + + private ProfileSupport() {} + + /** + * Kind of profile validation. + */ + public enum Validation { + /** + * Request to validate sources on source path. + */ + SOURCES, + + /** + * Request to validate binaries on compile class path by Profile attribute in Manifest. + * The manifest based validation does not analyze class files and is significantly faster + * compared to {@link Validation#BINARIES_BY_CLASS_FILES} but does not work for jar + * files with no Profile attributes. + */ + BINARIES_BY_MANIFEST, + + /** + * Request to validate binaries on compile class path by references in class files. + */ + BINARIES_BY_CLASS_FILES + } + + /** + * Violation of profile. + * The violation can be caused either by jar file on classpath requiring + * a higher profile in manifest or by a source or class file referring to + * a type from higher profile. + */ + public static class Violation { + + private final URL root; + private final Profile profile; + private final URL file; + private final ElementHandle type; + + private Violation( + @NonNull final URL root, + @NullAllowed final Profile profile, + @NullAllowed final URL file, + @NullAllowed final ElementHandle type) { + Parameters.notNull("root", root); //NOI18N + this.root = root; + this.profile = profile; + this.file = file; + this.type = type; + } + + /** + * Returns the root which violates the tested profile. + * @return the root {@link URL} + */ + @NonNull + public URL getRoot() { + return root; + } + + /** + * Returns the {@link Profile} required by a root or file. + * @return the {@link Profile} required by a root in the manifest + * or by class (source) file under the root. May return a null if the + * manifest contains invalid profile attribute. + */ + @CheckForNull + public Profile getRequiredProfile() { + return profile; + } + + /** + * Returns the file which violates the tested profile. + * @return the file referring to a type from higher profile. May return + * null if the whole archive requires a higher profile. + */ + @CheckForNull + public URL getFile() { + return file; + } + + /** + * Returns the type from higher {@link Profile} causing the violation. + * @return the type causing the violation or null if the whole archive + * requires a higher profile. + */ + @CheckForNull + public ElementHandle getUsedType() { + return type; + } + } + + /** + * Asynchronous callback for collecting the profile {@link Violation}s. + * For each classpath or source root a new {@link ViolationCollector} is + * created by the {@link ViolationCollectorFactory#create}. When the validation + * of the root is done the {@link ViolationCollector#finished} is called. + * Threading: Validations of individual roots may run in parallel depending + * on the {@link Executor} throughput. + */ + public interface ViolationCollector { + /** + * Called to report a {@link Profile} violation. + * @param violation the {@link Violation} to be reported. + */ + void reportProfileViolation(@NonNull Violation violation); + /** + * Called when the validation of whole a root has finished. + */ + void finished(); + } + + /** + * Factory for {@link ViolationCollector}. + * For each root a new {@link ViolationCollector} is created by the factory. + * Threading: Validations of individual roots may run in parallel depending + * on the {@link Executor} throughput. + */ + public interface ViolationCollectorFactory { + /** + * Creates a new {@lni ViolationCollector} for given root. + * @param root the root to be validated + * @return a new {@link ViolationCollector} + */ + @NonNull + ViolationCollector create(@NonNull URL root); + + /** + * Signals that the validation should be canceled. + * @return if true the validation is canceled. + */ + boolean isCancelled(); + } + + /** + * Asynchronously finds the {@link Profile} violations in given source and classpath roots. + * @param profileToCheck the {@link Profile} to be verified + * @param bootClassPath the boot classpath of JDK 8 platform to get the profile info from + * @param compileClassPath the compile classpath to be validated + * @param sourcePath the source path to be validated + * @param check types of validation + * @param collectorFactory the {@link Violation}s collector + * @throws IllegalArgumentException if the bootClassPath is not a valid JDK 8 boot classpath + */ + public static void findProfileViolations( + @NonNull final Profile profileToCheck, + @NonNull final Iterable bootClassPath, + @NonNull final Iterable compileClassPath, + @NonNull final Iterable sourcePath, + @NonNull final Set check, + @NonNull final ViolationCollectorFactory collectorFactory) { + findProfileViolations( + profileToCheck, + bootClassPath, + compileClassPath, + sourcePath, + check, + collectorFactory, + RP); + } + + /** + * Synchronously finds the {@link Profile} violations in given source and classpath roots. + * @param profileToCheck the {@link Profile} to be verified + * @param bootClassPath the boot classpath of JDK 8 platform to get the profile info from + * @param compileClassPath the compile classpath to be validated + * @param sourcePath the source path to be validated + * @param check types of validation + * @return the {@link Collection} of found {@link Violation}s + * @throws IllegalArgumentException if the bootClassPath is not a valid JDK 8 boot classpath + */ + @NonNull + public static Collection findProfileViolations( + @NonNull final Profile profileToCheck, + @NonNull final Iterable bootClassPath, + @NonNull final Iterable compileClassPath, + @NonNull final Iterable sourcePath, + @NonNull final Set check) { + final DefaultProfileViolationCollector collector = + new DefaultProfileViolationCollector(); + findProfileViolations( + profileToCheck, + bootClassPath, + compileClassPath, + sourcePath, + check, + collector, + new CurrentThreadExecutor()); + return collector.getViolations(); + } + + /** + * Asynchronously finds the {@link Profile} violations in given source and classpath roots. + * @param profileToCheck the {@link Profile} to be verified + * @param bootClassPath the boot classpath of JDK 8 platform to get the profile info from + * @param compileClassPath the compile classpath to be validated + * @param sourcePath the source path to be validated + * @param check types of validation + * @param collectorFactory the {@link Violation}s collector + * @param executor to use for the asynchronous operation, may have higher throughput + * @throws IllegalArgumentException if the bootClassPath is not a valid JDK 8 boot classpath + */ + public static void findProfileViolations( + @NonNull final Profile profileToCheck, + @NonNull final Iterable bootClassPath, + @NonNull final Iterable compileClassPath, + @NonNull final Iterable sourcePath, + @NonNull final Set check, + @NonNull final ViolationCollectorFactory collectorFactory, + @NonNull final Executor executor) { + Parameters.notNull("profileToCheck", profileToCheck); //NOI18N + Parameters.notNull("compileClassPath", compileClassPath); //NOI18N + Parameters.notNull("sourcePath", sourcePath); //NOI18N + Parameters.notNull("check", check); //NOI18N + Parameters.notNull("collectorFactory", collectorFactory); //NOI18N + Parameters.notNull("executor", executor); //NOI18N + final Context ctx = new Context(profileToCheck, bootClassPath, collectorFactory, check); + if (check.contains(Validation.BINARIES_BY_MANIFEST) || + check.contains(Validation.BINARIES_BY_CLASS_FILES)) { + for (final URL compileRoot : compileClassPath) { + executor.execute(Validator.forBinary(compileRoot, ctx)); + } + } + if (check.contains(Validation.SOURCES)) { + for (final URL sourceRoot : sourcePath) { + executor.execute(Validator.forSource(sourceRoot, ctx)); + } + } + } + + private static final class Context { + + private final ArchiveCache archiveCache; + private final TypeCache typeCache; + private final Profile profileToCheck; + private final ViolationCollectorFactory factory; + private final Set validations; + + Context( + @NonNull final Profile profileToCheck, + @NonNull final Iterable bootClassPath, + @NonNull final ViolationCollectorFactory factory, + @NonNull final Set validations) { + assert profileToCheck != null; + assert bootClassPath != null; + assert factory != null; + assert validations != null; + this.archiveCache = ArchiveCache.getInstance(); + this.typeCache = TypeCache.newInstance(bootClassPath); + this.profileToCheck = profileToCheck; + this.factory = factory; + this.validations = EnumSet.copyOf(validations); + } + + @NonNull + ArchiveCache getArchiveCache() { + return archiveCache; + } + + @NonNull + TypeCache getTypeCache() { + return typeCache; + } + + @NonNull + Profile getRequredProfile() { + return profileToCheck; + } + + @NonNull + ViolationCollector newCollector(@NonNull final URL root) { + return factory.create(root); + } + + boolean shouldValidate(@NonNull final Validation validation) { + return validations.contains(validation); + } + + boolean isCancelled() { + return factory.isCancelled(); + } + + } + + private static abstract class Validator implements Runnable { + + protected final Context context; + protected final URL root; + + + Validator( + @NonNull final URL root, + @NonNull final Context context) { + assert root != null; + assert context != null; + this.root = root; + this.context = context; + } + + @Override + public final void run() { + final ViolationCollector collector = context.newCollector(root); + assert collector != null; + try { + validate(collector); + } finally { + collector.finished(); + } + } + + protected final void validateBinaryRoot( + @NonNull final URL root, + @NonNull final ViolationCollector collector) { + final FileObject rootFo = URLMapper.findFileObject(root); + if (rootFo == null) { + return; + } + final Enumeration children = rootFo.getChildren(true); + while (children.hasMoreElements()) { + final FileObject fo = children.nextElement(); + if (isImportant(fo)) { + validateBinaryFile(fo, collector); + } + } + } + + @NonNull + protected URL map(@NonNull final FileObject fo) { + return fo.toURL(); + } + + protected abstract void validate(@NonNull ViolationCollector collector); + + private boolean isImportant(@NonNull final FileObject file) { + return file.isData() && + (FileObjects.CLASS.equals(file.getExt()) || FileObjects.SIG.equals(file.getExt())); + } + + private void validateBinaryFile( + @NonNull final FileObject fo, + @NonNull final ViolationCollector collector) { + final Profile profileToCheck = context.getRequredProfile(); + final TypeCache tc = context.getTypeCache(); + try { + try (InputStream in = fo.getInputStream()) { + ClassFile cf = new ClassFile(in); + for (ClassName className : cf.getAllClassNames()) { + final Profile p = tc.profileForType(className); + if (p != null && profileToCheck.compareTo(p) < 0) { + collector.reportProfileViolation( + new Violation( + root, + p, + map(fo), + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, className.getInternalName().replace('/', '.')) //NOI18N + )); + } + } + } + } catch (IOException ioe) { + LOG.log( + Level.INFO, + "Cannot validate file: {0}", //NOI18N + FileUtil.getFileDisplayName(fo)); + } + } + + static Validator forSource( + @NonNull final URL root, + @NonNull final Context context) { + return new SourceValidator(root, context); + } + + static Validator forBinary( + @NonNull final URL root, + @NonNull final Context context) { + return new BinaryValidator(root, context); + } + + private final static class BinaryValidator extends Validator { + + private BinaryValidator( + @NonNull final URL root, + @NonNull final Context context) { + super(root, context); + } + + @Override + protected void validate(@NonNull final ViolationCollector collector) { + Profile current = null; + if (context.shouldValidate(Validation.BINARIES_BY_MANIFEST)) { + final Union2 res = findProfileInManifest(root); + if (!res.hasFirst()) { + //Invalid value of profile in manifest of dependent jar + collector.reportProfileViolation(new Violation(root, null, null, null)); + return; + } + if (res.first().compareTo(context.getRequredProfile()) > 0) { + //Hiher profile in manifest of dependent jar + collector.reportProfileViolation(new Violation(root, res.first(), null, null)); + return; + } + current = res.first(); + } + if (context.shouldValidate(Validation.BINARIES_BY_CLASS_FILES)) { + if (current == null || current == Profile.DEFAULT) { + validateBinaryRoot(root, collector); + } + } + } + + @NonNull + private Union2 findProfileInManifest(@NonNull URL root) { + final ArchiveCache ac = context.getArchiveCache(); + Union2 res; + final ArchiveCache.Key key = ac.createKey(root); + if (key != null) { + res = ac.getProfile(key); + if (res != null) { + return res; + } + } + String profileName = null; + final FileObject rootFo = URLMapper.findFileObject(root); + if (rootFo != null) { + final FileObject manifestFile = rootFo.getFileObject(RES_MANIFEST); + if (manifestFile != null) { + try { + try (InputStream in = manifestFile.getInputStream()) { + final Manifest manifest = new Manifest(in); + final Attributes attrs = manifest.getMainAttributes(); + profileName = attrs.getValue(ATTR_PROFILE); + } + } catch (IOException ioe) { + LOG.log( + Level.INFO, + "Cannot read Profile attribute from: {0}", //NOI18N + FileUtil.getFileDisplayName(manifestFile)); + } + } + } + final Profile profile = Profile.forName(profileName); + res = profile != null ? + Union2.createFirst(profile) : + Union2.createSecond(profileName); + if (key != null) { + ac.putProfile(key, res); + } + return res; + } + + } + + private final static class SourceValidator extends Validator { + private SourceValidator( + @NonNull final URL root, + @NonNull final Context context) { + super(root, context); + } + + @Override + protected void validate(@NonNull final ViolationCollector collector) { + try { + final File cacheRoot = JavaIndex.getClassFolder(root, true); + if (cacheRoot != null) { + validateBinaryRoot(Utilities.toURI(cacheRoot).toURL(), collector); + } + } catch (IOException e) { + Exceptions.printStackTrace(e); + } + } + } + } + + + private static class CurrentThreadExecutor implements Executor { + @Override + public void execute(Runnable command) { + command.run(); + } + } + + private static class DefaultProfileViolationCollector implements ViolationCollectorFactory, ViolationCollector { + + private final Queue violations = new ArrayDeque<>(); + + @Override + public ViolationCollector create(@NonNull final URL root) { + return this; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void reportProfileViolation(@NonNull final Violation violation) { + violations.offer(violation); + } + + @Override + public void finished() { + } + + Collection getViolations() { + return Collections.unmodifiableCollection(violations); + } + } + + //@ThreadSafe + private static final class ArchiveCache { + + private static final int MAX_CACHE_SIZE = Integer.getInteger( + "ProfileSupport.ArchiveCache.size", //NOI18N + 1<<10); + + //@GuardedBy("ArchiveCache.class") + private static volatile ArchiveCache instance; + + //@GuardedBy("cache") + private final Map> cache; + + private ArchiveCache() { + this.cache = Collections.synchronizedMap(new LinkedHashMap>(16, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> entry) { + return size() > MAX_CACHE_SIZE; + } + }); + } + + @NonNull + static ArchiveCache getInstance() { + ArchiveCache cache = instance; + if (cache == null) { + synchronized (ArchiveCache.class) { + cache = instance; + if (cache == null) { + instance = cache = new ArchiveCache(); + } + } + } + return cache; + } + + @CheckForNull + Union2 getProfile(@NonNull final Key key) { + final Union2 res = cache.get(key); + if (LOG.isLoggable(Level.FINER)) { + LOG.log( + Level.FINER, + "cache[{0}]->{1}", //NOI18N + new Object[]{ + key, + res.hasFirst() ? res.first() : res.second() + }); + } + return res; + } + + void putProfile( + @NonNull final Key key, + @NonNull final Union2 profile) { + if (LOG.isLoggable(Level.FINER)) { + LOG.log( + Level.FINER, + "cache[{0}]<-{1}", //NOI18N + new Object[]{ + key, + profile.hasFirst() ? profile.first() : profile.second() + }); + } + cache.put(key,profile); + } + + @CheckForNull + Key createKey(@NonNull final URL rootURL) { + final URL fileURL = FileUtil.getArchiveFile(rootURL); + if (fileURL == null) { + //Not an archive + return null; + } + final FileObject fileFo = URLMapper.findFileObject(fileURL); + if (fileFo == null) { + return null; + } + return new Key( + fileFo.toURI(), + fileFo.lastModified().getTime(), + fileFo.getSize()); + } + + private static final class Key { + + private final URI root; + private final long mtime; + private final long size; + + Key( + @NonNull final URI root, + final long mtime, + final long size) { + this.root = root; + this.mtime = mtime; + this.size = size; + } + + @Override + public int hashCode() { + int hash = 17; + hash = 31 * hash + (this.root != null ? this.root.hashCode() : 0); + hash = 31 * hash + (int) (this.mtime ^ (this.mtime >>> 32)); + hash = 31 * hash + (int) (this.size ^ (this.size >>> 32)); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Key)) { + return false; + } + final Key other = (Key) obj; + return this.root.equals(other.root) && + this.mtime == other.mtime && + this.size == other.size; + + } + + @Override + public String toString() { + return String.format( + "Key{root: %s, mtime: %d, size: %d}", //NOI18N + root, + mtime, + size); + } + } + } + + //@ThreadSafe + private static final class TypeCache { + + private final Object UNKNOWN = new Object(); + private final ConcurrentMap cache; + private final Archive ctSym; + + private TypeCache(@NonNull final Archive ctSym) { + assert ctSym != null; + this.ctSym = ctSym; + cache = new ConcurrentHashMap<>(); + } + + @NonNull + static TypeCache newInstance(Iterable bootClassPath) { + Archive ctSym = null; + final CachingArchiveProvider ap = CachingArchiveProvider.getDefault(); + for (URL root : bootClassPath) { + if (ap.hasCtSym(root)) { + ctSym = ap.getArchive(root, true); + break; + } + } + if (ctSym == null) { + throw new IllegalArgumentException( + String.format( + "No profile info for boot classpath: %s", //NOI18N + bootClassPath)); + } + return new TypeCache(ctSym); + } + + @CheckForNull + Profile profileForType(@NonNull final ClassName className) { + final String binName = className.getInternalName(); + Object res = cache.get(binName); + if (res == null) { + res = findProfile(binName); + cache.put(binName, res); + } + return res == UNKNOWN ? null : (Profile) res; + } + + @NonNull + private Object findProfile(@NonNull final String binaryName) { + Object res = UNKNOWN; + final StringBuilder sb = new StringBuilder(binaryName); + sb.append('.'); //NOI18N + sb.append(FileObjects.CLASS); + try { + final JavaFileObject jfo = ctSym.getFile(sb.toString()); + if (jfo != null) { + try (InputStream in = jfo.openInputStream()) { + final ClassFile cf = new ClassFile(in); + final Annotation a = cf.getAnnotation(ClassName.getClassName(ANNOTATION_PROFILE)); + if (a == null) { + res = Profile.COMPACT1; + } else { + final AnnotationComponent ac = a.getComponent(ANNOTATION_VALUE); + res = profileFromAnnotationComponent(ac); + } + } + } + } catch (IOException ioe) { + Exceptions.printStackTrace(ioe); + } + return res; + } + + @NonNull + private static Profile profileFromAnnotationComponent(@NullAllowed final AnnotationComponent ac) { + if (ac == null) { + return Profile.COMPACT1; + } + try { + final ElementValue ev = ac.getValue(); + if (!(ev instanceof PrimitiveElementValue)) { + return Profile.COMPACT1; + } + final CPEntry cpEntry = ((PrimitiveElementValue)ev).getValue(); + if (cpEntry.getTag() != 3) { + return Profile.COMPACT1; + } + final int ordinal = (Integer) cpEntry.getValue(); + if (ordinal <= 0) { + return Profile.COMPACT1; + } + final Profile[] values = Profile.values(); + if (ordinal >= values.length) { + return Profile.DEFAULT; + } + return values[ordinal-1]; + } catch (NumberFormatException nfe) { + return Profile.COMPACT1; + } + } + } + +} --- a/java.source/src/org/netbeans/modules/java/source/parsing/CachingArchiveProvider.java +++ a/java.source/src/org/netbeans/modules/java/source/parsing/CachingArchiveProvider.java @@ -161,6 +161,12 @@ } } + /** + * Maps ct.sym back to the base boot classpath root. + * @param archiveOrCtSym the root or ct.sym folder to transform. + * @return the boot classpath root corresponding to the ct.sym folder + * or the given boot classpath root. + */ @NonNull public URL mapCtSymToJar (@NonNull final URL archiveOrCtSym) { final URI result = ctSymToJar.get(toURI(archiveOrCtSym)); @@ -175,6 +181,28 @@ } /** + * Checks if the given boot classpath root has the the ct.sym equivalent. + * @param root the root to check + * @return true if there is a ct.sym folder corresponding to given boot classpath + * root + */ + public boolean hasCtSym (@NonNull final URL root) { + final URL fileURL = FileUtil.getArchiveFile(root); + if (fileURL == null) { + return false; + } + final File f = Utilities.toFile(fileURL); + if (f == null || !f.exists()) { + return false; + } + synchronized (this) { + final Pair res = mapJarToCtSym(f, root); + return res.second != null; + } + + } + + /** * Frees all the archives. */ synchronized void clear() { --- a/java.source/test/unit/src/org/netbeans/api/java/source/support/ProfileSupportTest.java +++ a/java.source/test/unit/src/org/netbeans/api/java/source/support/ProfileSupportTest.java @@ -0,0 +1,411 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2013 Sun Microsystems, Inc. + */ +package org.netbeans.api.java.source.support; + + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.java.classpath.ClassPath; +import org.netbeans.api.java.platform.JavaPlatform; +import org.netbeans.api.java.platform.JavaPlatformManager; +import org.netbeans.api.java.platform.Specification; +import org.netbeans.api.java.queries.SourceLevelQuery; +import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.java.platform.JavaPlatformProvider; +import org.netbeans.modules.java.source.ElementHandleAccessor; +import org.netbeans.modules.java.source.parsing.FileObjects; +import org.netbeans.modules.java.source.usages.Pair; +import org.netbeans.spi.java.classpath.support.ClassPathSupport; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.modules.SpecificationVersion; +import org.openide.util.Utilities; + +/** + * + * @author Tomas Zezula + */ +public class ProfileSupportTest extends NbTestCase { + + private static final String JDK8Home = "/Users/tom/Projects/other/repos/jdk/jdk8/build/macosx-x86_64-normal-server-release/images/j2sdk-bundle/jdk1.8.0.jdk/Contents/Home"; + + public ProfileSupportTest(@NonNull final String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + clearWorkDir(); + MockServices.setServices(PP.class); + } + + + public void testProfilesByClassFiles() throws IOException { + final JavaPlatform jp = JavaPlatformManager.getDefault().getDefaultPlatform(); + if (jp == null || !jp.getSpecification().getVersion().equals(new SpecificationVersion("1.8"))) { //NOI18N + //Nothing to test + return; + } + final Map> testCp = createTestData(); + Collection res = ProfileSupport.findProfileViolations( + SourceLevelQuery.Profile.COMPACT1, + toURLs(jp.getBootstrapLibraries().entries()), + testCp.keySet(), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_CLASS_FILES)); + assertEquals( + new HashMap,SourceLevelQuery.Profile>>>(){ + { + final Collection files = testCp.entrySet().iterator().next().getValue(); + put( + files.iterator().next(), + Arrays.,SourceLevelQuery.Profile>>asList( + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "java.sql.Date"), + SourceLevelQuery.Profile.COMPACT2), + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "javax.naming.Context"), + SourceLevelQuery.Profile.COMPACT3), + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "org.omg.CORBA.ORB"), + SourceLevelQuery.Profile.DEFAULT) + )); + } + }, + res); + + res = ProfileSupport.findProfileViolations( + SourceLevelQuery.Profile.COMPACT2, + toURLs(jp.getBootstrapLibraries().entries()), + testCp.keySet(), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_CLASS_FILES)); + assertEquals( + new HashMap,SourceLevelQuery.Profile>>>(){ + { + final Collection files = testCp.entrySet().iterator().next().getValue(); + put( + files.iterator().next(), + Arrays.,SourceLevelQuery.Profile>>asList( + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "javax.naming.Context"), + SourceLevelQuery.Profile.COMPACT3), + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "org.omg.CORBA.ORB"), + SourceLevelQuery.Profile.DEFAULT) + )); + } + }, + res); + res = ProfileSupport.findProfileViolations( + SourceLevelQuery.Profile.COMPACT3, + toURLs(jp.getBootstrapLibraries().entries()), + testCp.keySet(), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_CLASS_FILES)); + assertEquals( + new HashMap,SourceLevelQuery.Profile>>>(){ + { + final Collection files = testCp.entrySet().iterator().next().getValue(); + put( + files.iterator().next(), + Arrays.,SourceLevelQuery.Profile>>asList( + Pair.,SourceLevelQuery.Profile>of( + ElementHandleAccessor.getInstance().create(ElementKind.CLASS, "org.omg.CORBA.ORB"), + SourceLevelQuery.Profile.DEFAULT) + )); + } + }, + res); + res = ProfileSupport.findProfileViolations( + SourceLevelQuery.Profile.DEFAULT, + toURLs(jp.getBootstrapLibraries().entries()), + testCp.keySet(), + Collections.emptySet(), + EnumSet.of(ProfileSupport.Validation.BINARIES_BY_CLASS_FILES)); + assertEquals( + Collections.,SourceLevelQuery.Profile>>>emptyMap(), + res); + } + + private void assertEquals( + Map,SourceLevelQuery.Profile>>> exp, + Collection res) { + final Map,SourceLevelQuery.Profile>>> resM = + new HashMap<>(); + for (ProfileSupport.Violation v : res) { + List,SourceLevelQuery.Profile>> e = resM.get(v.getFile()); + if (e == null) { + e = new ArrayList<>(); + resM.put(v.getFile(), e); + } + e.add(Pair.,SourceLevelQuery.Profile>of( + v.getUsedType(), + v.getRequiredProfile())); + } + for (List,SourceLevelQuery.Profile>> l : resM.values()) { + Collections.sort(l, new Comparator,SourceLevelQuery.Profile>>() { + @Override + public int compare( + Pair, SourceLevelQuery.Profile> o1, + Pair, SourceLevelQuery.Profile> o2) { + return o1.first.getBinaryName().compareTo(o2.first.getBinaryName()); + } + }); + } + assertEquals(exp, resM); + }; + + + @NonNull + private Collection toURLs(@NonNull final Collection entries) { + final Queue q = new ArrayDeque<>(); + for (ClassPath.Entry e : entries) { + q.offer(e.getURL()); + } + return q; + } + + private Map> createTestData() throws IOException { + final File workDir = getWorkDir(); + final File src = new File (workDir, "src"); //NOI18N + final File bin = new File (workDir, "bin"); //NOI18N + src.mkdirs(); + bin.mkdirs(); + final File testFile = new File (src,"Test.java"); //NOI18N + try (PrintWriter out = new PrintWriter(testFile)) { + out.println( + "public class Test {\n" + //NOI18N + " String s;\n" + //NOI18N + " java.sql.Date d;\n" + //NOI18N + " javax.naming.Context c;\n" +//NOI18N + " org.omg.CORBA.ORB orb;\n" +//NOI18N + "}\n"); //NOI18N + } + final JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); + final StandardJavaFileManager standardFileManager = jc.getStandardFileManager(null, null, null); + final StringWriter out = new StringWriter(); + final JavaCompiler.CompilationTask task = jc.getTask( + out, + standardFileManager, + null, + Arrays.asList( + "-s", src.getAbsolutePath(), //NOI18N + "-d", bin.getAbsolutePath(), //NOI18N + "-target", "1.8", //NOI18N + "-source", "1.8", //NOI18N + "-processorpath", ""), //NOI18N + Collections.emptySet(), + standardFileManager.getJavaFileObjects(testFile)); + final Boolean res = task.call(); + if (res != Boolean.TRUE) { + throw new IOException(out.toString()); + } + return Collections.>singletonMap( + Utilities.toURL(bin), + collectClassFiles(bin)); + } + + @NonNull + private Collection collectClassFiles(@NonNull final File folder) throws MalformedURLException { + final Queue q = new ArrayDeque<>(); + collectClassFiles(folder, q); + return q; + } + + private void collectClassFiles(@NonNull final File folder, @NonNull final Queue q) throws MalformedURLException { + final File[] chld = folder.listFiles(); + if (chld == null) { + return; + } + for (File f : chld) { + if (f.isDirectory()) { + collectClassFiles(f, q); + } else if (FileObjects.CLASS.equals(FileObjects.getExtension(f.getName()))) { + q.offer(Utilities.toURL(f)); + } + } + } + + + public static class PP implements JavaPlatformProvider { + + private final JavaPlatform jp; + + public PP () { + final Pair jdk8 = findJDK8(); + jp = jdk8 == null ? null : new JP(jdk8); + } + + + @Override + public JavaPlatform[] getInstalledPlatforms() { + return jp == null ? + new JavaPlatform[0] : + new JavaPlatform[] {jp}; + } + + @Override + public JavaPlatform getDefaultPlatform() { + return jp; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + @CheckForNull + private static Pair findJDK8() { + File jdkHome = null; + if (JDK8Home != null) { + jdkHome = new File(JDK8Home); + } else { + //TODO: + } + if (jdkHome == null || !jdkHome.isDirectory()) { + return null; + } + final File rtJar = new File( + jdkHome, + "jre/lib/rt.jar".replace('/', File.separatorChar)); //NOI18N + if (!rtJar.isFile()) { + return null; + } + return Pair.of( + FileUtil.normalizeFile(jdkHome), + FileUtil.urlForArchiveOrDir(FileUtil.normalizeFile(rtJar))); + } + + private static class JP extends JavaPlatform { + + private final FileObject home; + private final ClassPath boot; + + + JP(Pair p) { + this.home = FileUtil.toFileObject(p.first); + this.boot = ClassPathSupport.createClassPath(p.second); + } + + + @Override + public String getDisplayName() { + return "Mock Java Platform"; //NOI18N + } + + @Override + public Map getProperties() { + return Collections.emptyMap(); + } + + @Override + public ClassPath getBootstrapLibraries() { + return boot; + } + + @Override + public ClassPath getStandardLibraries() { + return ClassPath.EMPTY; + } + + @Override + public String getVendor() { + return "me"; //NOI18N + } + + @Override + public Specification getSpecification() { + return new Specification("j2se", new SpecificationVersion("1.8")); //NOI18N + } + + @Override + public Collection getInstallFolders() { + return Collections.singleton(home); + } + + @Override + public FileObject findTool(String toolName) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public ClassPath getSourceFolders() { + return ClassPath.EMPTY; + } + + @Override + public List getJavadocFolders() { + return Collections.emptyList(); + } + } + } +}