blob: 98bbe56420dc57c3bdd91745b59fb0dc8d00cdbf [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.java.project.ui;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
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.List;
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.logging.Logger;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
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.classpath.ClassPath;
import org.netbeans.api.java.platform.JavaPlatformManager;
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;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.ant.AntArtifact;
import org.netbeans.api.project.libraries.Library;
import org.netbeans.spi.project.support.ant.AntProjectHelper;
import org.netbeans.spi.project.support.ant.PropertyEvaluator;
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.FileUtil;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.netbeans.spi.project.support.ant.EditableProperties;
import org.netbeans.spi.project.ui.ProjectProblemResolver;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.Exceptions;
import org.openide.util.MutexException;
import static org.netbeans.modules.java.project.ui.Bundle.*;
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.FileObject;
import org.openide.util.ImageUtilities;
import org.openide.util.RequestProcessor;
import org.openide.util.WeakListeners;
/**
*
* @author Tomas Zezula
*/
final class ProfileProblemsProviderImpl implements ProjectProblemsProvider, PropertyChangeListener, ChangeListener, Runnable {
private static final String LIB_PREFIX = "${libs."; // NOI18N
private static final String PRJ_PREFIX = "${reference."; // NOI18N
private static final String FILE_PREFIX = "${file.reference."; //NOI18N
private static final String REF_PREFIX = "${"; //NOI18N
private static final String VOL_CLASSPATH = "classpath"; //NOI18N
private static final char PATH_SEPARATOR_CHAR = ':'; //NOI18N
private static final String ICON_LIBRARIES = "org/netbeans/modules/java/project/ui/resources/libraries.gif"; //NOI18N
private static final String ICON_FILE = "org/netbeans/modules/java/project/ui/resources/jar.gif";//NOI18N
private static final int SLIDING_DELAY = 1000; //1s
private static final Logger LOG = Logger.getLogger(ProfileProblemsProviderImpl.class.getName());
private static final RequestProcessor RP = new RequestProcessor(ProjectProblemsProviders.class);
private final AntProjectHelper antProjectHelper;
private final ReferenceHelper referenceHelper;
private final PropertyEvaluator evaluator;
private final String profileProperty;
private final Set<String> classPathProperties;
private final ProjectProblemsProviderSupport problemsProviderSupport;
//@GuardedBy("foreignSlResults")
private final Collection<SourceLevelQuery.Result> foreignSlResults;
private final RequestProcessor.Task firer;
private final Object listenersInitLock = new Object();
//@GuardedBy("listenersInitLock")
private ClassPath classPath;
//@GuardedBy("listenersInitLock")
private SourceLevelQuery.Result slRes;
ProfileProblemsProviderImpl(
@NonNull final AntProjectHelper antProjectHelper,
@NonNull final ReferenceHelper referenceHelper,
@NonNull final PropertyEvaluator evaluator,
@NonNull final String profileProperty,
@NonNull final String... classPathProperties) {
assert antProjectHelper != null;
assert referenceHelper != null;
assert evaluator != null;
assert profileProperty != null;
assert classPathProperties != null;
this.antProjectHelper = antProjectHelper;
this.referenceHelper = referenceHelper;
this.evaluator = evaluator;
this.profileProperty = profileProperty;
this.classPathProperties = new HashSet<String>(
Arrays.asList(classPathProperties));
this.problemsProviderSupport = new ProjectProblemsProviderSupport(this);
this.foreignSlResults = Collections.synchronizedCollection(new ArrayList<SourceLevelQuery.Result>());
this.firer = RP.create(this);
}
@Override
public void addPropertyChangeListener(@NonNull final PropertyChangeListener listener) {
Parameters.notNull("listener", listener); //NOI18N
problemsProviderSupport.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(@NonNull final PropertyChangeListener listener) {
Parameters.notNull("listener", listener); //NOI18N
problemsProviderSupport.removePropertyChangeListener(listener);
}
@Override
@NbBundle.Messages({
"LBL_InvalidProfile=Invalid JRE 8 Profile",
"DESC_InvalidProfile=The project profile ({0}) is lower than the profile of used libraries ({1}).",
"DESC_IllegalProfile=The project libraries have illegal value of profile."
})
public Collection<? extends ProjectProblem> getProblems() {
return problemsProviderSupport.getProblems(new ProjectProblemsProviderSupport.ProblemsCollector() {
@Override
public Collection<? extends ProjectProblem> collectProblems() {
return ProjectManager.mutex().readAccess(new Mutex.Action<Collection<? extends ProjectProblem>>() {
@Override
public Collection<? extends ProjectProblem> run() {
final SourceLevelQuery.Result mySL = listenenOnProjectMetadata();
final Profile profile = mySL.getProfile();
if (profile == Profile.DEFAULT) {
return Collections.<ProjectProblem>emptySet();
}
final Set<Reference> problems = collectReferencesWithWrongProfile(profile);
if (problems.isEmpty()) {
return Collections.<ProjectProblem>emptySet();
}
Profile minProfile = null;
for (Reference problem : problems) {
final Profile problemProfile = problem.getRequiredProfile();
minProfile = max(minProfile, problemProfile);
}
return Collections.<ProjectProblem>singleton(
ProjectProblem.createError(
LBL_InvalidProfile(),
minProfile != null
? DESC_InvalidProfile(profile.getDisplayName(), minProfile.getDisplayName())
: DESC_IllegalProfile(),
new ProfileResolver(
antProjectHelper,
profileProperty,
profile,
problems)));
}
});
}
});
}
@Override
public void propertyChange(@NonNull final PropertyChangeEvent event) {
final String propName = event.getPropertyName();
if (ClassPath.PROP_ROOTS.equals(propName) ||
JavaPlatformManager.PROP_INSTALLED_PLATFORMS.equals(propName)) {
firer.schedule(SLIDING_DELAY);
}
}
@Override
public void stateChanged(ChangeEvent ce) {
firer.schedule(SLIDING_DELAY);
}
@Override
public void run() {
problemsProviderSupport.fireProblemsChange();
}
private SourceLevelQuery.Result listenenOnProjectMetadata() {
synchronized (listenersInitLock) {
if (slRes == null) {
assert classPath == null;
final JavaPlatformManager jpm = JavaPlatformManager.getDefault();
jpm.addPropertyChangeListener(WeakListeners.propertyChange(this, jpm));
slRes = SourceLevelQuery.getSourceLevel2(antProjectHelper.getProjectDirectory());
slRes.addChangeListener(this);
final File baseFolder = FileUtil.toFile(antProjectHelper.getProjectDirectory());
if (baseFolder != null) {
final ClassPath cp = ClassPathFactory.createClassPath(ProjectClassPathSupport.createPropertyBasedClassPathImplementation(
baseFolder,
evaluator,
classPathProperties.toArray(new String[classPathProperties.size()])));
cp.addPropertyChangeListener(this);
cp.getRoots();
classPath = cp;
}
}
return slRes;
}
}
@NonNull
private Set<Reference> collectReferencesWithWrongProfile(@NonNull final Profile currentProfile) {
assert ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess();
final Set<Reference> res = new HashSet<Reference>();
final EditableProperties projectProps = antProjectHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
final EditableProperties privateProps = antProjectHelper.getProperties(AntProjectHelper.PRIVATE_PROPERTIES_PATH);
final EditableProperties globalProps = PropertyUtils.getGlobalProperties();
final Collection<SourceLevelQuery.Result> newSlResults = new ArrayDeque<SourceLevelQuery.Result>();
for (String cpId : classPathProperties) {
collectReferencesWithWrongProfileImpl(
cpId,
currentProfile,
antProjectHelper,
evaluator,
referenceHelper,
projectProps,
privateProps,
globalProps,
res,
newSlResults);
}
synchronized (foreignSlResults) {
for (final Iterator<? extends SourceLevelQuery.Result> it = foreignSlResults.iterator(); it.hasNext();) {
final SourceLevelQuery.Result cslr = it.next();
it.remove();
cslr.removeChangeListener(this);
}
assert foreignSlResults.isEmpty();
for (SourceLevelQuery.Result cslr : newSlResults) {
cslr.addChangeListener(this);
foreignSlResults.add(cslr);
}
}
return res;
}
private static void collectReferencesWithWrongProfileImpl(
@NonNull final String classPathId,
@NonNull final Profile requiredProfile,
@NonNull final AntProjectHelper antProjectHelper,
@NonNull final PropertyEvaluator eval,
@NonNull final ReferenceHelper refHelper,
@NonNull final EditableProperties projectProps,
@NonNull final EditableProperties privateProps,
@NonNull final EditableProperties globalProps,
@NonNull final Set<? super Reference> collector,
@NonNull final Collection<? super SourceLevelQuery.Result> slResCollector) {
String cp = projectProps.getProperty(classPathId);
if (cp == null) {
cp = privateProps.getProperty(classPathId);
}
if (cp == null) {
cp = globalProps.getProperty(classPathId);
}
if (cp != null) {
final Queue<String> todo = new ArrayDeque<String>(Arrays.asList(PropertyUtils.tokenizePath(cp)));
while (!todo.isEmpty()) {
final String rawEntry = todo.remove();
final String propName = getAntPropertyName(rawEntry);
if (propName == null) {
continue;
}
if (rawEntry.startsWith(LIB_PREFIX)) {
final String libName = rawEntry.substring(LIB_PREFIX.length(),rawEntry.lastIndexOf('.'));
final Library lib = refHelper.findLibrary(libName);
if (lib != null) {
final Collection<? extends ProfileSupport.Violation> res =
ProfileSupport.findProfileViolations(
requiredProfile,
Collections.<URL>emptySet(),
lib.getContent(VOL_CLASSPATH),
Collections.<URL>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);
if (ref[0] != null) {
AntArtifact artifact = (AntArtifact)ref[0];
final SourceLevelQuery.Result slRes = SourceLevelQuery.getSourceLevel2(artifact.getProject().getProjectDirectory());
slResCollector.add(slRes);
final Profile minProfile = slRes.getProfile();
if (isBroken(requiredProfile, minProfile)) {
collector.add(new ProjectReference(classPathId, rawEntry, minProfile, artifact.getProject()));
}
}
} else if (rawEntry.startsWith(FILE_PREFIX)) {
final String path = eval.getProperty(propName);
if (path != null) {
final File file = antProjectHelper.resolveFile(path);
final URL root = FileUtil.urlForArchiveOrDir(file);
if (root != null) {
final Collection<? extends ProfileSupport.Violation> res =
ProfileSupport.findProfileViolations(
requiredProfile,
Collections.<URL>emptySet(),
Collections.singleton(root),
Collections.<URL>emptySet(),
EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST));
if (!res.isEmpty()) {
final Profile maxProfile = findMaxProfile(res);
collector.add(new FileReference(classPathId, rawEntry, maxProfile, file));
}
}
}
} else if (rawEntry.startsWith(REF_PREFIX)) {
collectReferencesWithWrongProfileImpl(
propName,
requiredProfile,
antProjectHelper,
eval,
refHelper,
projectProps,
privateProps,
globalProps,
collector,
slResCollector);
} else {
final File file = antProjectHelper.resolveFile(propName);
final URL root = FileUtil.urlForArchiveOrDir(file);
if (root != null) {
final Collection<? extends ProfileSupport.Violation> res =
ProfileSupport.findProfileViolations(
requiredProfile,
Collections.<URL>emptySet(),
Collections.singleton(root),
Collections.<URL>emptySet(),
EnumSet.of(ProfileSupport.Validation.BINARIES_BY_MANIFEST));
if (!res.isEmpty()) {
final Profile maxProfile = findMaxProfile(res);
collector.add(new FileReference(classPathId, rawEntry, maxProfile, file));
}
}
}
}
}
}
private static boolean isBroken(
@NonNull final Profile requiredProfile,
@NullAllowed final Profile profile) {
if (profile == null) {
return true;
}
final Profile max = max(requiredProfile, profile);
return !max.equals(Profile.DEFAULT) &&
!max.equals(requiredProfile);
}
@CheckForNull
private static String getAntPropertyName(@NullAllowed String reference) {
if (reference != null
&& reference.startsWith("${") // NOI18N
&& reference.endsWith("}")) { // NOI18N
reference = reference.substring(2, reference.length() - 1);
}
return reference;
}
@CheckForNull
private static Profile findMaxProfile(@NonNull final Iterable<? extends ProfileSupport.Violation> violations) {
Profile current = null;
for (ProfileSupport.Violation violation : violations) {
Profile p = violation.getRequiredProfile();
if (p == null) {
//Broken profile - no need to continue
return null;
}
current = max(current, p);
}
return current;
}
@CheckForNull
static Profile max (@NullAllowed Profile a, @NullAllowed Profile b) {
if (b == null) {
return a;
}
if (a == null) {
return b;
}
return a.compareTo(b) <= 0 ?
b :
a;
}
@NonNull
static Profile requiredProfile(
@NonNull final Collection<? extends Reference> state,
@NonNull Profile initial) {
Profile current = initial;
for (ProfileProblemsProviderImpl.Reference re : state) {
current = max(current, re.getRequiredProfile());
}
return current;
}
abstract static class Reference {
private final String classPathId;
private final String rawId;
private final Profile requiredProfile;
private Reference(
@NonNull final String classPathId,
@NonNull final String rawId,
@NullAllowed final Profile requiredProfile) {
Parameters.notNull("classPathId", classPathId); //NOI18N
Parameters.notNull("rawId", rawId); //NOI18N
this.classPathId = classPathId;
this.rawId = rawId;
this.requiredProfile = requiredProfile;
}
@NonNull
abstract String getDisplayName();
@NonNull
abstract String getToolTipText();
@NonNull
abstract Icon getIcon();
@CheckForNull
final Profile getRequiredProfile() {
return requiredProfile;
}
private void remove(@NonNull final AntProjectHelper helper) {
final EditableProperties props = helper.getProperties (AntProjectHelper.PROJECT_PROPERTIES_PATH);
String rawPath = props.getProperty (classPathId);
if (rawPath != null) {
final String[] pathElements = PropertyUtils.tokenizePath(rawPath);
final List<String> result = new ArrayList<String>(pathElements.length);
boolean changed = false;
for (String pathElement : pathElements) {
if (rawId.equals(pathElement)) {
changed = true;
continue;
}
result.add(pathElement + PATH_SEPARATOR_CHAR);
}
if (!result.isEmpty()) {
final String last = result.get(result.size()-1);
result.set(result.size()-1, last.substring(0, last.length()-1));
}
if (changed) {
props.setProperty(classPathId, result.toArray(new String[result.size()]));
helper.putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, props);
}
}
}
@Override
public int hashCode() {
int hash = 17;
hash = 31 * hash + this.classPathId.hashCode();
hash = 31 * hash + this.rawId.hashCode();
return hash;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Reference)) {
return false;
}
final Reference otherRef = (Reference) other;
return classPathId.equals(otherRef.classPathId) &&
rawId.equals(otherRef.rawId);
}
}
private static final class LibraryReference extends Reference {
private final Library lib;
private LibraryReference(
@NonNull final String classPathId,
@NonNull final String rawId,
@NullAllowed final Profile requiredProfile,
@NonNull final Library lib) {
super(classPathId, rawId, requiredProfile);
Parameters.notNull("lib", lib); //NOI18N
this.lib = lib;
}
@Override
String getDisplayName() {
return this.lib.getDisplayName();
}
@Override
String getToolTipText() {
return lib.getDisplayName();
}
@Override
Icon getIcon() {
return ImageUtilities.loadImageIcon(ICON_LIBRARIES, false);
}
}
private static final class ProjectReference extends Reference {
private final Project prj;
private ProjectReference(
@NonNull final String classPathId,
@NonNull final String rawId,
@NullAllowed final Profile requiredProfile,
@NonNull final Project prj) {
super(classPathId, rawId, requiredProfile);
Parameters.notNull("prj", prj); //NOI18N
this.prj = prj;
}
@Override
String getDisplayName() {
return ProjectUtils.getInformation(prj).getDisplayName();
}
@Override
String getToolTipText() {
return FileUtil.getFileDisplayName(prj.getProjectDirectory());
}
@Override
Icon getIcon() {
return ProjectUtils.getInformation(prj).getIcon();
}
}
private static final class FileReference extends Reference {
private final File file;
private FileReference(
@NonNull final String classPathId,
@NonNull final String rawId,
@NullAllowed final Profile requiredProfile,
@NonNull final File file) {
super(classPathId, rawId, requiredProfile);
Parameters.notNull("file", file); //NOI18N
this.file = file;
}
@Override
String getDisplayName() {
return file.getName();
}
@Override
String getToolTipText() {
return file.getAbsolutePath();
}
@Override
Icon getIcon() {
return ImageUtilities.loadImageIcon(ICON_FILE, false);
}
}
private static final class ProfileResolver implements ProjectProblemResolver {
private final AntProjectHelper antProjectHelper;
private final String profileProperty;
private final Profile currentProfile;
private final Collection<? extends Reference> state;
private ProfileResolver(
@NonNull final AntProjectHelper antProjectHelper,
@NonNull final String profileProperty,
@NonNull final Profile currentProfile,
@NonNull final Collection<? extends Reference> state) {
assert antProjectHelper != null;
assert profileProperty != null;
assert currentProfile != null;
assert state != null;
this.antProjectHelper = antProjectHelper;
this.profileProperty = profileProperty;
this.currentProfile = currentProfile;
this.state = state;
}
@Override
@NbBundle.Messages({
"LBL_ResolveProfile=Resolve Invalid Project Profile",
"LBL_ResolveButton=OK",
"AN_ResolveButton=Resolve",
"AD_ResolveButton=Resolve the profile problems"
})
public Future<Result> resolve() {
final JButton ok = new JButton(LBL_ResolveButton());
ok.getAccessibleContext().setAccessibleName(AN_ResolveButton());
ok.getAccessibleContext().setAccessibleDescription(AD_ResolveButton());
final FixProfile panel = new FixProfile(ok, currentProfile, state);
final DialogDescriptor dd = new DialogDescriptor(
panel,
LBL_ResolveProfile(),
true,
new Object[]{
ok,
DialogDescriptor.CANCEL_OPTION
},
ok,
DialogDescriptor.DEFAULT_ALIGN,
null,
null);
if (DialogDisplayer.getDefault().notify(dd) == ok) {
return RP.submit(new Callable<ProjectProblemsProvider.Result>() {
@Override
public ProjectProblemsProvider.Result call() throws Exception {
ProjectProblemsProvider.Status status = ProjectProblemsProvider.Status.UNRESOLVED;
try {
ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
@Override
public Void run() throws IOException {
final boolean shouldUpdate = panel.shouldUpdateProfile();
if (shouldUpdate) {
final Profile newProfile = panel.getProfile();
final EditableProperties props = antProjectHelper.getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH);
if (newProfile == null ||
newProfile == Profile.DEFAULT) {
props.remove(profileProperty);
} else {
props.put(profileProperty, newProfile.getName());
}
antProjectHelper.putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, props);
}
for (Reference rootToRemove : panel.getRootsToRemove()) {
rootToRemove.remove(antProjectHelper);
}
ProjectManager.getDefault().saveProject(
FileOwnerQuery.getOwner(antProjectHelper.getProjectDirectory()));
return null;
}
});
status = ProjectProblemsProvider.Status.RESOLVED;
} catch (MutexException e) {
Exceptions.printStackTrace(e);
}
return ProjectProblemsProvider.Result.create(status);
}
});
}
final RunnableFuture<Result> res = new FutureTask<Result>(
new Callable<Result>() {
@Override
public Result call() {
return ProjectProblemsProvider.Result.create(ProjectProblemsProvider.Status.UNRESOLVED);
}
});
res.run();
return res;
}
@Override
public int hashCode() {
int res = 17;
final FileObject projDir = antProjectHelper.getProjectDirectory();
res = res * 31 + (projDir == null ? 0 : projDir.toURI().hashCode());
res = res * 31 + currentProfile.hashCode();
return res;
}
@Override
public boolean equals(@NullAllowed Object other) {
if (other == this) {
return true;
}
if (!(other instanceof ProfileResolver)) {
return false;
}
final ProfileResolver otherResolver = (ProfileResolver) other;
final FileObject projDir = antProjectHelper.getProjectDirectory();
final FileObject otherProjDir = otherResolver.antProjectHelper.getProjectDirectory();
return
currentProfile.equals(otherResolver.currentProfile) &&
(projDir == null ? otherProjDir == null : projDir.equals(otherProjDir));
}
}
}