/* * 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.j2seproject.problems; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Objects; 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.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JButton; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.java.platform.JavaPlatform; import org.netbeans.api.java.platform.JavaPlatformManager; import org.netbeans.api.java.queries.SourceLevelQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.java.api.common.project.ProjectProperties; import org.netbeans.modules.java.j2seproject.J2SEProject; import org.netbeans.modules.java.j2seproject.api.J2SECustomPropertySaver; import org.netbeans.modules.java.j2seproject.api.J2SERuntimePlatformProvider; import org.netbeans.modules.java.j2seproject.ui.customizer.J2SEProjectProperties; import org.netbeans.spi.project.ProjectServiceProvider; import org.netbeans.spi.project.support.ant.AntProjectHelper; import org.netbeans.spi.project.support.ant.EditableProperties; import org.netbeans.spi.project.ui.ProjectProblemResolver; import org.netbeans.spi.project.ui.ProjectProblemsProvider; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.filesystems.FileAttributeEvent; import org.openide.filesystems.FileChangeListener; import org.openide.filesystems.FileEvent; import org.openide.filesystems.FileLock; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileRenameEvent; import org.openide.filesystems.FileUtil; import org.openide.modules.SpecificationVersion; import org.openide.util.Exceptions; import org.openide.util.Mutex; import org.openide.util.NbBundle; import org.openide.util.Parameters; import org.openide.util.RequestProcessor; import org.openide.util.Union2; import org.openide.util.WeakListeners; /** * * @author Tomas Zezula */ @ProjectServiceProvider(service = ProjectProblemsProvider.class, projectType = "org-netbeans-modules-java-j2seproject") public class RuntimePlatformProblemsProvider implements ProjectProblemsProvider, PropertyChangeListener, FileChangeListener { private static final Logger LOG = Logger.getLogger(RuntimePlatformProblemsProvider.class.getName()); private static final RequestProcessor RP = new RequestProcessor(RuntimePlatformProblemsProvider.class); private static final String CFG_PATH = "nbproject/configs"; //NOI18N private final Project project; private final PropertyChangeSupport support; private final AtomicBoolean listens; //@GuardedBy("this") private long eventId; //@GuardedBy("this") private Collection<? extends ProjectProblem> problemCache; public RuntimePlatformProblemsProvider(@NonNull final Project project) { Parameters.notNull("project", project); //NOI18N this.project = project; this.support = new PropertyChangeSupport(this); this.listens = new AtomicBoolean(); } @Override public void addPropertyChangeListener(@NonNull final PropertyChangeListener listener) { Parameters.notNull("listener", listener); //NOI18N this.support.addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(@NonNull final PropertyChangeListener listener) { Parameters.notNull("listener", listener); //NOI18N this.support.removePropertyChangeListener(listener); } @NonNull @Override public Collection<? extends ProjectProblem> getProblems() { Collection< ? extends ProjectProblem> problems; long currentId; synchronized (this) { problems = problemCache; currentId = eventId; } if (problems == null) { initListeners(); problems = findProblems(); synchronized (this) { if (eventId == currentId) { problemCache = problems; } else if (problemCache != null) { problems = problemCache; } } } assert problems != null; return problems; } @Override public void propertyChange(@NonNull final PropertyChangeEvent evt) { Parameters.notNull("evt", evt); //NOI18N final String propName = evt.getPropertyName(); if (JavaPlatformManager.PROP_INSTALLED_PLATFORMS.equals(propName) || ProjectProperties.JAVAC_TARGET.equals(propName) || ProjectProperties.JAVAC_PROFILE.equals(propName)) { resetAndFire(); } } @Override public void fileChanged(FileEvent fe) { resetAndFire(); } @Override public void fileDataCreated(FileEvent fe) { resetAndFire(); } @Override public void fileDeleted(FileEvent fe) { resetAndFire(); } @Override public void fileRenamed(FileRenameEvent fe) { resetAndFire(); } @Override public void fileFolderCreated(FileEvent fe) { } @Override public void fileAttributeChanged(FileAttributeEvent fe) { } private void initListeners() { if (listens.compareAndSet(false, true)) { final JavaPlatformManager jpm = JavaPlatformManager.getDefault(); jpm.addPropertyChangeListener(WeakListeners.propertyChange( this, jpm)); J2SEProject j2sePrj = project.getLookup().lookup(J2SEProject.class); if (j2sePrj != null) { j2sePrj.evaluator().addPropertyChangeListener(this); } else { LOG.log( Level.WARNING, "No property evaluator provider in project {0}({1})", //NOI18N new Object[]{ ProjectUtils.getInformation(project).getDisplayName(), FileUtil.getFileDisplayName(project.getProjectDirectory()) }); } final FileObject projectFolder = project.getProjectDirectory(); if (projectFolder != null) { final File projectDir = FileUtil.toFile(projectFolder); if (projectDir != null) { final File cfgDir = new File(projectDir, CFG_PATH); //NOI18N FileUtil.addFileChangeListener(this, cfgDir); } } } } private void resetAndFire() { synchronized (this) { problemCache = null; eventId++; } support.firePropertyChange(PROP_PROBLEMS, null, null); } @NonNull private Collection<? extends ProjectProblem> findProblems() { return ProjectManager.mutex().readAccess(new Mutex.Action<Collection<? extends ProjectProblem>>() { @Override public Collection<? extends ProjectProblem> run() { final Collection<ProjectProblem> collector = new HashSet<>(); final FileObject prjDir = project.getProjectDirectory(); if (prjDir != null) { final FileObject cfgFolder = prjDir.getFileObject(CFG_PATH); if (cfgFolder != null) { final Set<JavaPlatform> allowedPlatforms = new HashSet<>(); final SourceLevelQuery.Result slqr = SourceLevelQuery.getSourceLevel2(prjDir); final String sourceLevel = slqr.getSourceLevel(); final SourceLevelQuery.Profile profile = slqr.getProfile(); if (sourceLevel != null) { for (J2SERuntimePlatformProvider rpp : project.getLookup().lookupAll(J2SERuntimePlatformProvider.class)) { allowedPlatforms.addAll(rpp.getPlatformType(new SpecificationVersion(sourceLevel), profile)); } for (FileObject cfgFile : cfgFolder.getChildren()) { if (!cfgFile.hasExt("properties")) { //NOI18N continue; } try { final EditableProperties ep = new EditableProperties(true); try (final InputStream in = cfgFile.getInputStream()){ ep.load(in); } final String runtimePlatform = ep.getProperty(J2SEProjectProperties.PLATFORM_RUNTIME); if (runtimePlatform != null && !runtimePlatform.isEmpty()) { final JavaPlatform platform = findPlatform(runtimePlatform); if (platform == null) { final RuntimePlatformResolver resolver = new RuntimePlatformResolver( project, cfgFile.getName(), ep.getProperty("$label"), //NOI18N runtimePlatform); collector.add(ProjectProblem.createError( NbBundle.getMessage(RuntimePlatformProblemsProvider.class, "LBL_MissingRuntimePlatform"), NbBundle.getMessage(RuntimePlatformProblemsProvider.class, "DESC_MissingRuntimePlatform", resolver.getDisplayName()), resolver)); } else if (!allowedPlatforms.contains(platform)) { final RuntimePlatformResolver resolver = new RuntimePlatformResolver( project, cfgFile.getName(), ep.getProperty("$label"), //NOI18N platform, new SpecificationVersion(sourceLevel), profile); collector.add(ProjectProblem.createError( NbBundle.getMessage(RuntimePlatformProblemsProvider.class, "LBL_InvalidRuntimePlatform"), NbBundle.getMessage(RuntimePlatformProblemsProvider.class, "DESC_InvalidRuntimePlatform", resolver.getDisplayName()), resolver)); } } } catch (IOException e) { Exceptions.printStackTrace(e); } } } } } return Collections.unmodifiableCollection(collector); } }); } private static JavaPlatform findPlatform(@NonNull final String platformId) { for (JavaPlatform jp : JavaPlatformManager.getDefault().getInstalledPlatforms()) { if (platformId.equals(jp.getProperties().get(J2SEProjectProperties.PROP_PLATFORM_ANT_NAME))) { return jp; } } return null; } @NonNull static SourceLevelQuery.Profile getPlatformProfile(@NonNull final JavaPlatform jp) { SourceLevelQuery.Profile profile = SourceLevelQuery.Profile.forName(jp.getProperties().get("netbeans.java.profile")); //NOI18N if (profile == null) { profile = SourceLevelQuery.Profile.DEFAULT; } return profile; } static final class InvalidPlatformData { private final JavaPlatform jp; private final SpecificationVersion targetLevel; private final SourceLevelQuery.Profile profile; InvalidPlatformData( @NonNull final JavaPlatform jp, @NonNull final SpecificationVersion targetLevel, @NonNull final SourceLevelQuery.Profile profile) { Parameters.notNull("jp", jp); //NOI18N Parameters.notNull("targetLevel", targetLevel); //NOI18N Parameters.notNull("profile", profile); //NOI18N this.jp = jp; this.targetLevel = targetLevel; this.profile = profile; } @NonNull JavaPlatform getJavaPlatform() { return jp; } @NonNull SpecificationVersion getTargetLevel() { return targetLevel; } @NonNull SourceLevelQuery.Profile getProfile() { return profile; } } private static final class RuntimePlatformResolver implements ProjectProblemResolver { private final Project prj; private final String cfgId; private final String cfgDisplayName; private final Union2<String,InvalidPlatformData> data; private RuntimePlatformResolver( @NonNull final Project project, @NonNull final String cfgId, @NullAllowed final String cfgDisplayName, @NonNull final Union2<String,InvalidPlatformData> data) { Parameters.notNull("project", project); //NOI18N Parameters.notNull("cfgId", cfgId); //NOI18N Parameters.notNull("data", data); //NOI18N this.prj = project; this.cfgId = cfgId; this.cfgDisplayName = cfgDisplayName == null ? cfgId : cfgDisplayName; this.data = data; } RuntimePlatformResolver( @NonNull final Project project, @NonNull final String cfgId, @NullAllowed final String cfgDisplayName, @NonNull final String platformId) { this( project, cfgId, cfgDisplayName, Union2.<String,InvalidPlatformData>createFirst(platformId)); } RuntimePlatformResolver( @NonNull final Project project, @NonNull final String cfgId, @NullAllowed final String cfgDisplayName, @NonNull final JavaPlatform jp, @NonNull final SpecificationVersion targetLevel, @NonNull final SourceLevelQuery.Profile profile) { this( project, cfgId, cfgDisplayName, Union2.<String,InvalidPlatformData>createSecond( new InvalidPlatformData(jp, targetLevel, profile))); } String getDisplayName() { return cfgDisplayName; } @Override public Future<Result> resolve() { final ResolveBrokenRuntimePlatform panel = data.hasFirst() ? ResolveBrokenRuntimePlatform.createMissingPlatform( prj, data.first()) : ResolveBrokenRuntimePlatform.createInvalidPlatform( prj, data.second()); final OK okButton = new OK(panel); final DialogDescriptor dd = new DialogDescriptor( panel, NbBundle.getMessage(RuntimePlatformProblemsProvider.class, "TITLE_MissingRuntimePlatform"), true, new Object[] {DialogDescriptor.CANCEL_OPTION, okButton}, okButton, DialogDescriptor.DEFAULT_ALIGN, null, null); if (DialogDisplayer.getDefault().notify(dd) == okButton) { final boolean isSourceLevelChange = panel.isDowngradeSourceLevel(); final String newPlatformId = panel.isSpecificPlatform() ? panel.getRuntimePlatform().getProperties().get(J2SEProjectProperties.PROP_PLATFORM_ANT_NAME) : null; final FutureTask<Result> res = new FutureTask<>(new Callable<Result>() { @Override public Result call() throws Exception { if (isSourceLevelChange) { return resolveSourceLevelImpl(data.second().jp); } else { return resolvePlatformImpl(newPlatformId); } } }); RP.post(res); return res; } final RunnableFuture<Result> res = new FutureTask<>( new Runnable() { @Override public void run() { } }, Result.create(Status.UNRESOLVED)); res.run(); return res; } @Override public boolean equals(@NullAllowed final Object obj) { if (obj == this) { return true; } if (!(obj instanceof RuntimePlatformResolver)) { return false; } final RuntimePlatformResolver other = (RuntimePlatformResolver) obj; final FileObject thisPrjDir = prj.getProjectDirectory(); final FileObject otherPrjDir = other.prj.getProjectDirectory(); return thisPrjDir == null ? otherPrjDir == null : thisPrjDir.equals(otherPrjDir) && data.equals(other.data); } @Override public int hashCode() { int res = 17; res = res * 31 + Objects.hashCode(prj.getProjectDirectory()); res = res * 31 + data.hashCode(); return res; } @NonNull private Result resolvePlatformImpl(@NullAllowed final String newPlatformId) throws Exception { return ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Result>() { @Override public Result run() throws Exception { FileObject prjDir = prj.getProjectDirectory(); if (prjDir != null) { final FileObject cfgDir = prjDir.getFileObject(CFG_PATH); if (cfgDir != null) { FileObject cfg = cfgDir.getFileObject(cfgId, "properties"); //NOI18N if (cfg != null) { final EditableProperties ep = new EditableProperties(true); try (final InputStream in = cfg.getInputStream()) { ep.load(in); } if (newPlatformId == null) { ep.remove(J2SEProjectProperties.PLATFORM_RUNTIME); } else { ep.setProperty(J2SEProjectProperties.PLATFORM_RUNTIME, newPlatformId); } final FileLock lock = cfg.lock(); try (OutputStream out = cfg.getOutputStream(lock)) { ep.store(out); } finally { lock.releaseLock(); } for (J2SECustomPropertySaver saver : prj.getLookup().lookupAll(J2SECustomPropertySaver.class)) { saver.save(prj); } return Result.create(Status.RESOLVED); } } } return Result.create(Status.UNRESOLVED); } }); } @NonNull private Result resolveSourceLevelImpl(@NonNull final JavaPlatform jp) throws Exception { return ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction<Result>() { @Override public Result run() throws Exception { final J2SEProject j2sePrj = prj.getLookup().lookup(J2SEProject.class); if (j2sePrj != null) { final EditableProperties props = j2sePrj.getUpdateHelper().getProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH); props.setProperty(ProjectProperties.JAVAC_SOURCE, jp.getSpecification().getVersion().toString()); props.setProperty(ProjectProperties.JAVAC_TARGET, jp.getSpecification().getVersion().toString()); final SourceLevelQuery.Profile profile = getPlatformProfile(jp); if (profile == SourceLevelQuery.Profile.DEFAULT) { props.remove(ProjectProperties.JAVAC_PROFILE); } else { props.setProperty(ProjectProperties.JAVAC_PROFILE, profile.getName()); } j2sePrj.getUpdateHelper().putProperties(AntProjectHelper.PROJECT_PROPERTIES_PATH, props); ProjectManager.getDefault().saveProject(prj); } return Result.create(Status.UNRESOLVED); } }); } } private static class OK extends JButton implements ChangeListener { private ResolveBrokenRuntimePlatform panel; OK (@NonNull final ResolveBrokenRuntimePlatform panel) { super(NbBundle.getMessage(RuntimePlatformProblemsProvider.class,"LBL_OK")); Parameters.notNull("panel", panel); this.panel = panel; panel.addChangeListener(this); stateChanged(null); } @Override public void stateChanged(ChangeEvent e) { setEnabled(panel.hasValidData()); } } }